本节书摘来自华章出版社《Spark大数据分析:核心概念、技术及实践》一书中的第2章,第2.2节,作者[美] 穆罕默德·古勒(Mohammed Guller),更多章节内容可以访问云栖社区“华章计算机”公众号查看。
2.2 Scala基础
Scala是一门支持面向对象编程和函数式编程的混合语言。它支持函数式编程的概念,比如不可变数据结构,把函数视为一等公民。在面向对象方面,它也支持类、对象、特质、封装、继承、多态和其他面向对象的概念。
Scala是一门静态类型语言。Scala应用程序是由Scala编译器编译的。Scala是类型安全的,Scala编译器在编译期间会强制检查类型安全性。这有助于减少应用程序中的bug。
最后,Scala是一门基于JVM的语言。Scala编译器会将Scala应用程序编译成Java字节码,Java字节码运行在JVM上。从字节码的层面来看,Scala应用程序和Java应用程序没有区别。
因为Scala是基于JVM的,所以它能和Java无缝互操作。在Java应用程序中可以使用Scala开发的库。更重要的是,Scala应用程序可以使用任何Java库,而无须任何包装器或胶水代码。Scala应用程序可以直接受益于近二十年来人们用Java开发出来的各种现有的库。
尽管Scala是一门混合了面向对象编程和函数式编程的语言,但是它还是侧重于函数式编程。这一点使它成为一门强大的语言。相比于把Scala当成面向对象语言使用而言,把Scala当成函数式编程语言来用更能让你受益匪浅。
本书无法覆盖Scala的方方面面。想要说明白Scala的每个细节需要一本更厚的书才行。本书只介绍编写Spark应用程序所需要的基础知识。假设你有一定的编程经验,故本书不会介绍编程的基础知识。
Scala是一门强大的语言。伴随着强大性而来的是它的复杂性。有些人想要立刻学会Scala的所有语言特性,因而被吓坏了。然而,你并不需要知道它的每个细节,就能高效地使用它。只要你学会了本章所涵盖的基础知识,你就能开始高效地开发Scala应用程序了。
2.2.1 起步
学习一门语言的最好方式就是使用它。如果你能运行一下附带的示例代码,你将更好地理解本章的内容。
可以用任何的文本编辑器编写Scala代码,用scalac编译它,用Scala执行。或者也可以使用由Typesafe提供的基于浏览器的IDE。当然,也可以使用基于Eclipse的Scala IDE、Intellij IDEA或NetBeans IDE。可以从www.scala-lang.org/download下载Scala二进制文件、Typesafe Activator和上述各种IDE。
学习Scala最快捷的途径就是使用Scala解释器,它提供了一个交互式的shell用于编写代码。它是一个REPL(读取、求值、输出、循环)工具。当你在Scala shell中输入一个表达式时,它会对其求值,将结果输出到控制台上,然后等待你输入下一个表达式。安装交互式Scala shell就像下载Scala二进制文件然后解压它一样简单。Scala shell的名字是scala。它位于bin目录下,只须在终端输入Scala就能启动它。
现在,你应当看到如图2-1所示的Scala shell提示符。
图2-1 Scala shell提示符
现在,可以输入任何Scala表达式。下面是一个例子。
在你按下回车键之后,Scala解释器就会执行代码,然后将结果输出在控制台上。可以用Scala shell执行本章的示例代码。
让我们开始学习Scala吧。
2.2.2 基础类型
和其他的编程语言类似,Scala提供一些基础类型和作用在它们之上的操作。Scala中的基础类型清单如表2-1所示。
表2-1 Scala中的基础变量类型
变 量 类 型 描 述 变 量 类 型 描 述
Byte 8位有符号整数 Double 32位双精度浮点数
Short 16位有符号整数 Char 16位无符号
Int 32位有符号整数 Unicode 字符
Long 64位有符号整数 String 字符串
Float 32位单精度浮点数 Boolean true或false
需要注意的是,Scala并没有基本类型。Scala的每一个基础类型都是一个类。当把一个Scala应用程序编译成Java字节码的时候,编译器会自动把Scala基础类型转变成Java基本类型,这样有助于提高应用程序的性能。
2.2.3 变量
Scala有两种类型的变量:可变和不可变的。不过,尽量不要使用可变变量。纯函数式程序是不会使用可变变量的。然而,有时使用可变变量会使代码更简单。因此,Scala也支持可变变量。当然,使用它的时候要小心。
可变变量使用关键字var声明,不可变变量使用关键字val声明。
val与命令式编程语言(如C/C++和Java)中的变量类似。它可以在创建之后重新赋值。创建、修改可变变量的语法如下。
val在初始化之后就不可以重新赋值了。创建val的语法如下。
上面的代码在添加了下面这行语句之后会发生什么呢?
编译器会报错。
需要着重指出的是,Scala编译器提供了种种便利。首先,以分号作为语句结尾是可选的。其次,有必要的话,编译器会进行类型推断。Scala是一门静态类型的语言,所以一切都是有类型的。然而,在Scala编译器能自动推导出类型的情况下,开发者不需要为它声明类型。使用Scala编程会使得代码更简短精炼。
下面两个语句是等价的。
2.2.4 函数
如前所述,函数是一段可执行的代码,这段代码最终返回一个值。它和数学中函数的概念类似,读取输入,然后返回一个输出。
Scala把函数当成一等公民。函数可以当成变量使用。它可以作为输入传递给其他函数。它也可以定义成匿名函数字面量,就像字符串字面量一样。它可以作为变量的值。它也可以在其他函数的函数体内定义。它还可以作为其他函数的返回值返回。
Scala中用关键字def来定义函数。函数定义以函数名开头,紧跟着是以逗号作为分隔符的输入参数列表,每个参数后面跟着它们各自的类型,参数列表放在一对圆括号中。在右圆括号后面是一个冒号、函数返回值类型、等号和函数体。函数体可以被大括号包裹,没有亦可。下面是一个例子。
在上面的例子中,函数名是add。它有两个都是Int类型的参数,返回一个Int类型的返回值。这个函数只是将两个参数相加,而后将累加和作为返回值返回。
这个函数可以简化成下面这样。
这个简化版的函数和之前的是一样的。返回值的类型省略了,因为编译器可以根据代码推导出来。然而,还是不推荐省略返回值类型。
简化版中大括号同样也省略了。只有当函数体中有不止一条语句时,才需要大括号。
关键字return也省略了,因为它是可选的。在Scala中,所有语句都是表达式,表达式总是会返回一个值。因此,简化版函数的返回值就是函数体中最后一个语句作为表达式所返回的值。
上面的代码片段只是一个例子,说明了使用Scala可以写出简洁的代码,提高代码的可读性和可维护性。
Scala支持多种类型的函数,这一点在下面进行介绍。
方法
方法是指类的成员函数。它的定义和使用方法和函数类似,唯一的区别是它可以访问类里面所有的成员。
局部函数
在其他函数中或在方法中定义的函数称为局部函数。它仅可使用输入参数和包含它的函数内的变量。它只在包含其定义的函数内可见。这一实用的特性使得你可以在函数内聚合多条语句,而不会污染整个应用程序的命名空间。
高阶方法
把函数作为输入参数的方法被称为高阶方法。类似的,高阶函数指的是把函数作为参数的函数。高阶方法和高阶函数有助于减少重复代码,从而使得代码更简洁。
下面是一个高阶函数的例子。
encode函数接受两个参数,返回一个Long类型的值。第一个参数是Int类型的。第二个参数是一个函数f,它接受一个Int类型的参数,返回一个Long类型的值。encode函数首先将第一个参数乘以10,然后将结果作为参数调用函数f。
在介绍Scala集合的时候我们会看到更多的高阶函数。
函数字面量
函数字面量是指源代码中的匿名函数。在应用程序中,可以像字符串字面量一样使用它。它可以作为高阶方法或高阶函数的参数,也可以赋值给变量。
函数字面量的定义由处于圆括号中的输入函数列表、右箭头和函数体构成。包裹函数体的大括号是可选的。下面是一个函数字面量的例子。
如果函数体只由一条语句构成,那么大括号是可以省略的。上面定义的函数字面量的简化版如下所示。
之前定义的高阶函数encode可以被当成函数字面量使用,如下所示。
闭包
在函数对象的函数体中,只能使用参数和函数字面量中定义的局部变量。然而,Scala中函数字面量却可以使用其所处作用域中的变量。闭包就是这种可以使用了非参数非局部变量的函数字面量。有时候人们把闭包和函数字面量当成同一术语,但是从技术上说,它们是不一样的。
下面是一个闭包的例子。
在上面的代码中,局部函数encode的第二个参数是个函数。这个函数字面量使用了两个变量n和seed。n是函数的参数,而seed却不是。在这个作为函数encode参数的函数字面量中,seed是从其所处的作用域获得的,并用在函数体中。
2.2.5 类
类是面向对象编程中的概念。它是一种高层的编程抽象。简单地说,它是一种将数据和操作结合在一起的代码组织方式。在概念上,它用属性和行为来表示一个实体。
Scala中的类和其他面向对象编程语言中的类似。它由字段和方法构成。字段就是一个变量,用于存储数据。方法就是一段可执行的代码,是在类中定义的函数。方法可以访问类中的所有字段。
类就是一个在运行期间创建对象的模板。对象就是一个类实例。类在源代码中定义,而对象只存在于运行期间。Scala使用关键字class来定义一个类。类的定义以类名开头,紧跟着参数列表,参数列表以逗号作为分隔符,然后是处于大括号中的字段和方法。
下面是一个例子。
类实例使用关键字new创建。
类通常用作可变数据结构。对象都有一个随时变化的状态。因此,类中的字段一般都是可变变量。
因为Scala运行在JVM之上,所以你不必显式删除对象。Java的垃圾回收器会自动删除那些不再使用的对象。
2.2.6 单例
在面向对象编程中一个常见的设计模式就是单例,它是指那些只可以实例化一次的类。Scala使用关键字object来定义单例对象。
2.2.7 样本类
样本类是指使用case修饰符的类,下面是一个例子。
对于样本类,Scala提供了一些语法上的便利。首先,样本类会添加与类名一致的工厂方法。因此,不必使用new关键字就可以创建一个样本类的类实例。举例来说,下面的这段代码就是合法的。
其次,样本类参数列表中的所有参数隐式获得val前缀。换句话说,Scala把上面定义的样本类Message当成如下定义。
val前缀把类参数转变成了不可变的类字段。故可以从外部访问它们。
最后,Scala为样本类添加了方法toString、hashCode、equals、copy。这些方法使得样本类便于使用。
在创建不可变对象时样本类相当有用,而且样本类还支持模式匹配。模式匹配将在下面进行介绍。
2.2.8 模式匹配
模式匹配是Scala中的概念,它看上去类似于其他语言的switch语句。然而,它却是一个比switch语句要强大得多的工具。它就像瑞士军刀一样能解决各种各样的问题。
模式匹配的一个简单用法就是替代多层的if-else语句。如果代码中有多于两个分支的if-else语句,它就难以阅读了。在这种场景下,使用模式匹配能提高代码的可读性。
作为一个例子,考虑这样一个简单的函数,它以表示颜色的字符串作为参数,如果是红色返回1,如果是蓝色返回2,如果是绿色返回3,如果是黄色返回4,如果是其他颜色返回0。
Scala使用关键字match来替代关键字switch。每一个可能的选项前面都跟着关键字case。如果有一个选项匹配,那么该选项右箭头右边的代码将会执行。下划线表示默认选项。如果任何选项都不匹配,那么默认选项对应的代码就会执行。
上面的例子虽然简单,但是它说明模式匹配的几个特性。首先,一旦有一个选项匹配,那么该选项对应的代码就会执行。不同于switch语句,每一个选项对应的代码中不需要有break语句。匹配选项之后的其他选项对应代码并不会被执行。
其次,每一个选项对应的代码都是表达式,表达式返回一个值。因此,模式匹配语句本身就是一个返回一个值的表达式。下面的代码说明了这一点。
2.2.9 操作符
Scala为基础类型提供了丰富的操作符。然而,Scala没有内置操作符。在Scala中,每一个基础类型都是一个类,每一个操作符都是一个方法。使用操作符等价于调用方法。考虑下面的例子。
+并不是Scala的内置操作符。它是定义在Int类中的一个方法。上面代码中的最后一条语句等价于如下代码。
Scala允许以操作符的方式来调用方法。
2.2.10 特质
特质是类继承关系中的接口。它的这种抽象机制有助于开发者写出模块化、可复用、可扩展的代码。
从概念上说,一个接口可以定义多个方法。Java中的接口只有函数签名,没有实现。继承这个接口的类必须实现这些接口方法。
Scala的特质类似于Java中的接口。然后,不同于Java中的接口,Scala特质可以有方法的实现。而且它还可以有字段。这样,继承类就可以复用这些字段和特质中实现的方法了。
特质看上去像是抽象类,它们都有字段和方法。区别在于一个类只能继承一个抽象类,但是可以继承多个特质。
下面是一个特质的例子。
2.2.11 元组
元组是一个容器,用于存放两个或多个不同类型的元素。它是不可变的。它自从创建之后就不能修改了。它的语法简单,如下所示。
当你想要把一些不相关的元素聚合在一起时,元组就派上用场了。当所有元素都是同一类型时,可以使用集合,比如数组或列表。当元素是不同类型但是之间有联系时,可以使用类,把它们当成类字段来存储。但是在某些场景下使用类没有必要。比如你想要有一个有多个返回值的函数,此时元组比类更合适。
元组的下标从1开始。下面这个例子展示怎么访问元组中的元素。
2.2.12 Option类型
Option是一种数据类型,用来表示值是可选的,即要么无值要么有值。它要么是样本类Some的实例,要么是单例对象None的实例。Some类的实例可以存储任何类型的数据,用来表示有值。None对象表示无值。
Option类型可以在函数或方法中作为值返回。返回Some(x)表示有值,x是真正的返回值。返回None表示无值。从函数返回的Option类型对象可以用于模式匹配中。
下面的例子说明了这些用法。
使用Option类型有助于避免空指针异常。在很多语言中,null用于表示无值。以C/C++/Java中一个返回整数的函数为例,如果对于给定的参数没有合法的整数可以返回,函数可能返回null。调用者如果没有检查返回值是否为null,而直接使用,就可以能导致程序崩溃。在Scala中,由于有了严格类型检查和Option类型,这样的错误得以避免。
2.2.13 集合
集合是一种容器类的数据结构,可以容纳零个或多个元素。它是一种抽象的数据结构。它支持声明式编程。它有方便使用的接口,使用这些接口就不必手动遍历所有元素了。
Scala有丰富的集合类,集合类包含各种类型。所有的集合类都有同样的接口。因此只要你熟悉了其中的一种集合,对于其他的集合类型你也能熟练使用。
Scala的集合类可以分为三类:序列、集合、map。本节将介绍Scala最常用的集合类。
序列
序列表示有先后次序的元素集合。由于元素是有次序的,因此可以根据位置来访问集合中的元素。举例来说,可以访问序列中第n个元素。
数组
数组是一个有索引的元素序列。数组中的所有元素都是相同类型的。它是可变的数据结构。可以修改数组中的元素,但是你不能在它创建之后增加元素。它是定长的。
Scala数组类似其他语言中的数组。访问其中的任意一个元素都占用固定的时间。数组的索引从0开始。要访问元素或修改元素,可以通过索引值达成,索引值位于括号内。下面是一个例子。
关于数组的基本操作如下:
通过索引值访问元素
通过索引值修改元素
列表
列表是一个线性的元素序列,其中存放一堆相同类型的元素。它是一种递归的结构,而不像数组(扁平的数据结构)。和数组不同,它是不可变的,创建后即不可修改。列表是Scala和其他函数式语言中最常使用的一种数据结构。
尽管可以根据索引来访问列表中的元素,但这并不高效。访问时间和元素在列表中的的位置成正比。
下面的代码展示了几种创建列表的方式。
关于列表的基本操作如下:
访问第一个元素。为此,List类提供了一个叫作head的方法。
访问第一个元素之后的所有元素。为此,List类提供了一个叫作tail的方法。
判断列表是否为空。为此,List类提供了一个叫作isEmpty的方法,当列表为空时,它返回true。
向量
向量是一个结合了列表和数组各自特性的类。它拥有数组和列表各自的性能优点。根据索引访问元素占用固定的时间,线性访问元素也占用固定的时间。向量支持快速修改和访问任意位置的元素。
下面是一个例子。
集合
集合是一个无序的集合,其中的每一个元素都不同。它没有重复的元素,而且,也没法通过索引来访问元素,因为它没有索引。
下面是一个例子。
集合支持两种基本操作。
contains:如果当前集合包含这个元素,则返回true。元素作为参数传递进来。
isEmpty:如果当前集合为空,则返回true。
map
map是一个键-值对集合。在其他语言中,它也叫作字典、关联数组或hash map。它是一个高效的数据结构,适合根据键找对应的值。千万不要把它和Hadoop MapReduce中的map混淆了。Hadoop MapReduce中的map指在集合上的一种操作。
下面这段代码展示了如何创建并使用map。
Scala还有其他集合类,这里就不一一介绍了。然而,只要掌握了以上内容,这些就足以高效地使用Scala了。
集合类上的高阶方法
Scala集合的强大之处就在于这些高阶方法。这些高阶方法把函数当成参数。需要注意的是,这些高阶方法并没有改变集合。
本节将介绍一些常用的高阶方法。例子中使用的是List集合,但是所有的Scala集合类都支持这些高阶方法。
map
map方法的参数是一个函数,它将这个函数作用于集合中的每一个元素,返回由其返回值所组成的集合。这个返回的集合中的元素个数和调用map的集合元素个数一致。然而,返回集合中的元素类型有可能不一样。
下面是一个例子。
在上面的例子中,需要注意的是,xs的类型是List[Int],而ys的类型是List[Double]。
如果一个函数只有一个参数,那么包裹参数列表的圆括号可以用大括号代替。下面的两条语句是等价的。
就像之前说的一样,Scala允许以操作符的方式调用任何方法。为了进一步提高了代码的可读性。上面的代码可以改写成如下这样。
Scala会根据集合中元素类型对函数字面量中的参数进行类型推导,故可以省略参数类型。下面的两条语句是等价的。
如果函数字面量的参数只在函数体内使用一次,那么右箭头及其左边部分都可以省略。可以只写函数字面量的主体。下面的两条语句是等价的。
下划线表示集合中的元素,它作为参数传递给map中的函数字面量。上面的代码可以解读为将xs中的每个元素乘以10。
总之,下面分别是详细版和简化版的代码。
如你所见,使用Scala能很方便地写出易读的简洁代码。
flatMap
Scala集合的flatMap方法类似于map,它的参数是一个函数,它把这个函数作用于集合中的每一个元素,返回另外一个集合。这个函数作用于原集合中的一个元素之后会返回一个集合。这样,最后就会得到一个元素都是集合的集合。使用map方法,就是这样的结果。但是使用flatMap会得到一个扁平化的集合。
下面是一个使用flatMap的例子。
toList方法将创建一个列表,里面包含原有集合的所有元素。这个方法能将字符串、数组、集合或其他集合类型转变成一个列表。
filter
filter方法将谓词函数作用于集合中的每个元素,返回另一个集合,其中只包含计算结果为真的元素。谓词函数指的是返回一个布尔值的函数。它要么返回true,要么返回false。
foreach
foreach方法的参数是一个函数,它把这个函数作用于集合中的每一个元素,但是不返回任何东西。它和map类似,唯一的区别在于map返回一个集合,而foreach不返回任何东西。由于它的无返回值特性它很少使用。
reduce
reduce方法返回一个值。顾名思义,它将一个集合整合成一个值。它的参数是一个函数,这个函数有两个参数,并返回一个值。从本质上说,这个函数是一个二元操作符,并且满足结合律和交换律。
下面是一些例子。
下面是一个找出句子中最长单词的例子。
需要注意的是,Hadoop MapReduce中的map/reduce和我们上面说的map/reduce是相似的。事实上,Haddoop MapReduce借用了函数式编程中的这些概念。