点击上方“大数据与人工智能”,“星标或置顶公众号”
第一时间获取好内容
本篇文章为大家带来Scala面试指南,本文会结合数据分析工程师在工作中会用到的知识点和网络上搜集的Scala常用考点,组成一份Scala精选题库,并附上详细的解答,力图为Scala面试者扫清知识盲点,提炼经典考题。
最简单和重要的理由是开发需要,大数据分析工程师是需要掌握大数据相关组件的,而很多大数据组件是由Scala开发的-如Spark和Kafka,所以相关的开发,Scala就成为了首选开发语言。
Scala作为一门面向对象的函数式编程语言,如其官网宣称的:“Object-OrientedMeets Functional”,这一句当属对Scala最抽象的精准描述,它把近二十年间大行其道的面向对象编程与旧而有之的函数式编程有机结合起来,形成其独特的魔力,可以使你的代码更简洁高效易于理解。同样的需求,不同水平的Scala工程师写出来的代码会有很大不同,所以考察Scala代码能力就能大致看出其编程水平。
学过Scala的同学肯定都会吐槽Scala难学,它将面向对象和函数式编程这两种不同的软件工程范式结合起来,它还有一个复杂的类型系统,所以对于Scala的考察涉及到的知识点非常多。想要通过Scala的面试,除了平时在学习和工作中的总结以外,刷题是一个很好的办法,本文会结合数据分析工程师工作中需要掌握的知识点做一个筛选,最终挑选出如下的考题,主要分为问答题和手写题,仔细看看有没有你不知道的知识点?
语言特性类
这一部分可能会考察Scala语言特性、相关概念类的知识点,或者与其他语言的比较,考察的是对Scala语言宏观上的认知。下面给出了经常会被问到的两道题目,其他相关的知识点-如与其他语言的比较,可以自行延伸补充。Scala 是一种有趣的语言。它一方面吸收继承了多种语言中的优秀特性,一方面又没有抛弃 Java 这个强大的平台,它运行在 JVM 之上,轻松实现和丰富的 Java 类库互联互通。它既支持面向对象的编程方式,又支持函数式编程。它写出的程序像动态语言一样简洁,但事实上它却是严格意义上的静态语言。总结起来就是面向对象和函数式的完美结合。简单来说,"函数式编程"是一种编程范式(programming paradigm),也就是如何编写程序的方法论。它属于结构化编程的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。1)代码简洁,开发快速,大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快;2)接近自然语言,易于理解,函数式编程的自由度很高,可以写出很接近自然语言的代码;3)代码管理方便,一个函数只需要给相应的参数,最终返回一个值,它不依赖也不改变外界的状态,只为了完成相应的运算,所以一个函数可以看做一个独立的单元,这样就方便单元测试和组合。4)易于并发操作,不修改变量也就不存在锁线程的问题,可以放心地用于并发编程。变量相关的知识点会涉及修饰符、类型、参数等概念,以及如何定义变量、关键字的区别等问题,是比较基础的知识点,主要还是在于多总结然后进行理解。var是变量声明关键字,类似于Java中的变量,变量值可以更改,但是变量类型不能更改。Scala 访问修饰符基本和Java的一样,分别有:private,protected,public。如果没有指定访问修饰符,默认情况下,Scala 对象的访问级别都是 public。Unit类型类似于Java中的void类型,代表没有任何意义的值类型。6Scala类型系统中Nil,Null,None,Nothing四种类型的区别?
在Scala中这四个类型名称很类似,作用却是完全不同的。下图是Scala类型图,有助于理解它们间的区别。 Nil代表一个List空类型,等同List[Nothing],根据List的定义List[+A],所有Nil是所有List[T]的子类;Null是所有AnyRef的子类,在Scala的类型系统中,AnyRef是Any的子类,同时Any子类的还有AnyVal, null是Null的唯一对象;None是一个object,是Option的子类型,代表没有值;Nothing是所有类型的子类,也是Null的子类。Nothing没有对象,但是可以用来定义类型。例如,如果一个方法抛出异常,则异常的返回值类型就是Nothing(虽然不会返回)。当函数的参数个数有多个,或者不固定的时候,可以使用vararg参数,具体的使用方式是在参数类型后面加一个“*”,相应的参数在函数体中就是一个集合,根据需要对参数进行解析。如果要传入的参数在集合中,可以将这个集合作为参数传入,需要在集合名后面加上“:_*”。从表达式开始涉及的知识点会越来越复杂,会涉及到匹配表达式、循环、正则,另外也要关注if……else、值绑定和通配符如何使用。8说说你对匹配表达式/模式匹配的理解?什么是模式守卫?
匹配表达式也就是match操作,类似C和Java中的“switch”语句,逐个匹配case表达式中的值,然后进行返回。模式校位目的是为匹配表达式增加条件逻辑,具体的做法是在case后面的匹配语句增加一个if表达式。Scala中的yield的主要作用是记住每次迭代中的有关值,并逐一存入到一个数组中。用法如下:for {子句} yield {变量或表达式},yield后面的语句其实就是一个循环体,只不过最终会将所有的循环结果放到一个集合中进行返回。Scala 的正则表达式继承了 Java 的语法规则,Java 则大部分使用了 Perl 语言的规则。Scala 通过 Scala.util.matching 包中的 Regex 类来支持正则表达式,参考示例代码如下:import scala.util.matching.Regex
val pattern = "Scala".r
val str = "Scala is Scalable and cool"
println(pattern findFirstIn str)
//执行结果:Some(Scala)
常用的匹配运算有:findFirstIn-找到首个匹配项,findAllIn-找到所有匹配项,replaceFirstIn-找到首个匹配项并进行替换,replaceAllIn-找到所有的匹配项并进行替换。函数在Scala中是一等公民,对这一块的考察应该是最多的,函数如何定义?什么是方法?偏函数、闭包、科里化等概念如何理解?高阶函数有哪些?什么是尾递归?什么是部分应用函数?除此之外也要关注嵌套函数、函数字面量、传名参数这些知识点。方法是定义在类中的函数,这个类进行实例化后会有一个同名的方法,一般调用方法的做法是使用缀点记法-实例名.方法名(参数……)有偏函数也有全函数,全函数是指能够正确地支持满足输入参数类型的所有可能的值,而偏函数是指不能够完全支持满足输入参数类型的可能的值,如果输入了这样的值函数无法正常工作。比如一个开平方根的函数输入的是一个负值,那么函数就无法工作。高阶函数在数据分析中使用到的频率是很高的,可能你辛苦一天写的函数代码,一个高阶函数轻松搞定。首先高阶函数肯定是函数,不同的是输入的参数和返回的值这两项中的一项必须是函数才能叫高阶函数。这个问题在回答的时候可以稍微拓展一下,介绍一下常用的的高阶函数,比如:map、flatMap、filter、reduce、fold。14什么是尾递归?
正常的递归,每一次递归操作,需要保存信息到堆栈中,当递归步骤达到一定量的时候,就可能会导致内存溢出,而尾递归,就是为了解决这样的问题,在尾递归中所有的计算都是在递归之前调用,也就是说递归一次计算一次,编译器可以利用这个属性避免堆栈错误,尾递归的调用可以使信息不插入堆栈,从而优化尾递归。
简单的理解就是:函数内部的变量不在其作用域时,仍然可以从外部进行访问。一般的构成是在嵌套函数中,内部的函数体可以访问外部函数体的变量,在本质上,闭包是将函数内部和函数外部连接起来的桥梁。部分应用函数可以从字面含义进行解释,只使用一个函数的部分功能-只使用部分参数,其他参数的值固定,可以将原函数直接调用,然后对于需要固定的参数,直接在参数中输入相应的值,需要变化的参数使用“_”,需要注意的是通配符要指定类型,这样生成的一个新函数就是部分应用函数。柯里化指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有的第二个参数作为参数的函数,所以科里化是一种返回函数的函数,目的是简化参数,是函数编写更加简洁和趋向自然语言。def mulNormal(x:Int,y:Int) = x * y //该函数每次执行需要两个参数
def mulOneAtTime(x:Int) = (y:Int) => x * y //该函数先接受一个参数生成另外一个接受单个参数的函数
这样的话,如果需要计算两个数的乘积的话只需要调用:mulOneAtTime(5)(4)18call-by-value和call-by-name求值策略的区别?
1)call-by-value是在调用函数之前计算;集合虽然种类有限,但是如果不注意区分还是很容易弄混,需要掌握不同集合的特点、使用场景、常用的集合函数、集合间的转换等。这个问题主要考察集合的基础知识,说出常用的集合,并对每种集合的特征加以描述就可以了。List列表:以线性方式存储,集合中可以存放重复对象;Set集合:集合中的对象不按特定的方式排序,并且没有重复对象;Map映射:是一种把键对象和值对象映射的集合,它的每一个元素都包含一对键对象和值对象;Option选项:单元素集合,用来表示一个值是可选的(有值或无值);可变集合可以在适当的地方被更新或扩展,这意味着你可以修改,添加,移除一个集合的元素。而不可变集合类,相比之下,永远不会改变。不过,你仍然可以模拟添加,移除或更新操作。但是这些操作将在每一种情况下都返回一个新的集合,同时原来的集合不发生改变。collection.mutable.Buffercollection.immutable.List集合之间是很容易相互转换的,根据具体的需要调用相应的方法进行转换,如:toList、toMap、toSet。Scala在JVM上编译运行的时候需要与JDK以及其他Java库进行交互,这部分的交互就会涉及到Scala和Java集合之间转换,默认情况下这两个集合是不兼容的,所以在代码中需要增加如下命令:1)import collection.JavaConverters._这样就增加了asJava和asScala这两个操作来实现它们集合之间的转换。2)import collection.JavaConversions._这里引入的是scala与java集合的隐式转换,就不需要特意进行asJava和asScala的转换,直接使用Java或者Scala的方法。Array是一个大小固定的可变索引集合。Scala中集合是不包括Array的,Array类型实际上是Java数组类型的一个包装器。Array中的第一个元素角标是0。Scala Iterator(迭代器)不是一个集合,它是一种用于访问集合的方法。迭代器 it 的两个基本操作是 next 和 hasNext。调用 it.next() 会返回迭代器的下一个元素,并且更新迭代器的状态。调用 it.hasNext() 用于检测集合中是否还有元素。Option类型表示一个值的存在与否,一般在程序中需要返回一个空对象的时候,使用Option类型,如果返回null,程序会引起异常,而Option就不会。使用Option减少触发NullPointerException异常的可能性。26Option,Try和Either三者的区别?
Option表示可选值,它的返回类型是Some(代表返回有效数据)或None(代表返回空值)。Try类似于Java中的try/catch,如果计算成功,返回Success的实例,如果抛出异常,返回Failure,try中是需要捕获异常的执行程序。Either可以提供一些计算失败的信息,Either有两种可能返回类型:预期/正确/成功的 或 错误的信息。对于面向对象的考察更多是概念,如对象、类、抽象类、单例对象、伴生对象、构造器、特质,如何继承?还需要关注重载、apply/unapply方法、包装语法。1)object是类的单例对象,开发人员无需用new关键字实例化2)object不能提供构造器参数,也就是说object必须是无参的。3)main方法只能在object中有效,Scala 中没有 static 关键字,对于一个class来说,所有的方法和成员变量在实例被 new 出来之前都是无法访问的因此class文件中的main方法也就没什么用了,而object中所有成员变量和方法默认都是static的,所以object中可以直接访问main方法。在Scala中,单例对象object与class名称相同时,该对象被称为该类的伴生对象,该类被称为该对象的伴生类。伴生类和伴生对象要处在同一个源文件中,伴生对象和伴生类可以互相访问其私有成员,不与伴生类同名的对象称之为孤立对象。29类的参数加和不加关键字(val和var)有区别吗?
有区别的,不加关键字的话,这个参数只能用于类的实例化,一旦实例化后这些参数就不可以使用了,如果加关键字的话这些参数就成为类中的一个字段。case class是不可实例化的类,一旦构建了这个类,它会自动生成一些方法和伴生对象,注意的是这个伴生对象也会自动生成一些自己的方法。它具有以下特性:(2)类中的参数默认添加val关键字,即参数不能修改,如果需要的话也可以使用var;(3)默认实现了toString,equals,hashcode,copy,apply,unapply等方法;31abstract class(抽象类)和trait(特质)的区别?
在Scala工程中抽象类和特质是很有用的工具,这个问题需要先回答什么是抽象类以及什么是特质。抽象类是在普通类的基础上增加了abstract关键字,无法对其进行实例化,它是用来被子类继承的,抽象类中可以只定义字段和方法,具体的值和实现在其子类中实现,子类也可以进行重写。特质是一种特殊的类,它支持多重继承,但是trait不能有类参数也不能实例化。(1)一个类只能继承一个抽象类,但是可以通过with关键字继承多个特质;Scala类的扩展只支持一个父类,要想实现多重继承有两种方法:1)多次扩展,假设4个类A、B、C、D——D继承于C,C继承于B、B继承于A,那么类D实例化后就可以使用A、B、C类中的变量和方法了,曲线实现了多重继承;2)使用特质-Trait,这是比较常用的方法,通过with关键字将多个特质加入,达到多重继承的目的,读取特质的顺序是从右往左。当需要某个类中的一个方法,但是这个类没有提供这样的一个方法,需要进行类型转换,转换成提供了这个方法的类,然后再调用这个方法,想要这个类型转换自动完成,就需要提前定义隐式转换函数,这样在使用要转换类型的方法的时候就可以自动转换。这个隐式转换函数可以通过导入相关的包来完成-比如java和Scala几个之间的相互转换就可以导入Scala.collection.JavaConversions类中的函数来实现,也可以自己编写。所谓隐式参数,指的是在函数或者方法中,定义使用implicit修饰的参数。当调用该函数或方法时,如果没有传该参数的值,Scala会尝试在变量作用域中找到一个与指定类型相匹配的使用implicit修饰的对象,即隐式值,注入到函数参数中函数体使用。值得注意的是,隐式参数是根据类型匹配的,因此作用域中不能同时出现两个相同类型的隐式变量,否则编译时会抛出隐式变量模糊的异常。35如何处理异常?
Scala通过捕获异常,捕获后可以进行处理,或者抛出给上游程序,抛出异常的方法和 Java一样,使用 throw 关键字。如要要对一段代码的执行进行异常检测,使用try将这段代码包起来,在catch语句中进行异常的匹配,借用了模式匹配的思想catch语句中是一系列的case字句。需要注意的是与try……catch成对出现的还有finally语句-用于执行不管是正常处理还是有异常发生时都需要执行的步骤。
通过手写代码(纸质试卷或者上机编码)来考查应聘者的编码能力是很常用的做法,手写代码直接考查面试者平时的代码积累,大多数人平时写代码都是使用开发工具-例如IDEA,会有代码提示、自动补全等功能,加快了编码者的工作效率,但是同时也弱化了手写代码能力,如果是去面试还是要强化一下这方面的能力。以下试题大部分参考《快学Scala》课后习题,可以用来检验下自己的手写代码能力。01
编写一个函数printDown(n:Int),使用模式匹配,打印从n到0的数字。
分析:本题考查的知识点是函数的定义、模式匹配的使用、循环的使用注意点:要考虑 n<0 的情况。import scala.language.postfixOps//程序执行的时候需要导入的包
def printDown(n: Int) {
n match {
case n if n >= 0 => {
(0 to n reverse) foreach println
}
case n if n < 0 => {
n to 0 foreach println
}
}
}
02
编写一段程序, 从文件中读取单词, 用一个可变映射来清点每一个单次出现的频率,读取这些单词的操作可以使用java.util.Scanner:
val in = new java.util.Scanner(new java.io.File("myfile.txt:)),while(in.hasNext()) 处理 in.next() 最后, 打印出所有单次和它们出现的次数。
分析:本题考查了函数的使用、读取文件、可变集合Map、迭代器、循环,统计单词的个数在很多编程题中都会出现。
import java.io.File
import java.util.{Scanner, StringTokenizer}
def wordCount(path: String): Scala.collection.mutable.Map[String, Int] = {
import Scala.collection.mutable.Map
val res = Map[String, Int]()
val in = new Scanner(new File(path))
while (in.hasNext()) {
val st = new StringTokenizer(in.next())
while (st.hasMoreTokens()) {
val key = st.nextToken()
res(key) = res.getOrElse(key, 0) + 1
}
}
res
}
03
编写一个Time类, 加入只读属性hours和minutes, 和一个检查某一时刻是否早于另一时刻的方法before(other: Time): Boolean。Time对象应该以new Time(hrs, min)方式构建,其中hrs小时数以军用时间格式呈现(介于0和23之间)。
分析:本题考查类的构建
def timeDemo(hr: Int, min: Int): Unit = {
class Time(hr: Int, min: Int) {
val hour = hr
val minute = min
def before(t: Time): Boolean = {
if (hour < t.hour) true else if (hour > t.hour) false else if (minute < t.minute) true else false
}
def <(t: Time): Boolean = before(t)
}
println(new Time(1, 2) < new Time(2, 1))
}
04
编写一个函数,接受一个字符串的集合,以及一个字符串到整型值的映射,返回整型集合, 其值为能和集合中某个字符串相对应的映射的值。举例来说,给定Array("Tom","Fred","Harry")和Map("Tom"->3,"Dick"->4,"Harry"->5),返回Array(3,5), 提示: 用flatMap将get返回的Option值组合在一起。
def highOrderFunction(arr: Array[String], map: Map[String, Int]): Array[Int] = {
arr.flatMap (map.get _ )
}
highOrderFunction(Array("a", "b"), Map("a" -> 100, "b" -> 2)) foreach println
1)比较相邻的元素,如果第一个比第二个大,就交换; 2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对;4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较;// 内层循环做排序
def bSort(data: Int, dataSet: List[Int]): List[Int] = dataSet match {
case List() => List(data)
case head :: tail => if (data <= head) data :: dataSet else head :: bSort(data, tail)
}
// 外层循环做拆分
def bubbleSort(l: List[Int]): List[Int] = l match {
case List() => List()
case head :: tail => bSort(head, bubbleSort(tail))
}
val list = List(97, 12, 43, 33, 7, 1, 2, 20)
println(bubbleSort(list))
//执行结果:List(1, 2, 7, 12, 20, 33, 43, 97
本篇Scala面试指南通过精选题库的方式将数据分析工作中涉及到的知识点尽可能完整地分布到问题中,涉及到的Scala知识点有基础的也有概念比较绕的,在手写代码类中的题目难易结合,希望这篇文章能够帮助准备面试大数据分析相关岗位的数据从业者查漏补缺,完善自己的Scala知识库。[1] Scala学习手册,作者:Jason Swartz
[2] 快学Scala,作者:Cay S. Horstmann
[3] 【译】Scala面试问题(Scala interview questions),作者:IIGEOywq - https://www.jianshu.com/p/ace2bb24dc11
[4] scala面试题总结,作者:郭小白 - https://www.cnblogs.com/Gxiaobai/p/10460336.html
-end-