参考http://dblab.xmu.edu.cn/blog/spark/ 林子雨老师的Spark入门教程中Scala语言的部分。
为什么使用Scala语言
- Scala是多范式编程语言,集成了面向对象和函数式语言的特性。运行与JVM虚拟机上,并兼容现有的Java程序。Scala代码可以调用Java方法,访问Java字段,集成Java类和实现Java接口。Scala是一门纯粹的面向对象编程语言。
- Scala具备强大的并发性,支持函数式编程,可以更好地支持分布式系统
- Scala语法简介,能够提供优雅的API
- Scala兼容Java,运行速度快,且能够融合到Hadoop生态圈
- Scala相对Java支持交互编程,而Java必须要编译才能运行。
Scala安装
Scala需要运行在JVM上,因此必须要安装Java JDK。
在macOS下安装
brew install scala
就可以直接安装scala
Linux下安装
- 在scala官网上下载scala安装包
- tar 解压到安装目录
tar xvzf scala.tgz -C dest_dir
- 将scala命令添加到环境变量(可以放在/etc/profile)中
export PATH=$PATH:/usr/local/scala/bin
- 在命令行中检查scala是否能找到该命令
Scala编程
Scala与Java编程的区别
1 | object HelloWorld{ |
(1)Scala程序中同样要定义程序入口main()函数。但与Java不同的是,在Java中,main函数必须是静态函数 public static void main(String[] args)
,而Scala中必须使用对象方法。
(2)在Scala中对象的命名不一定要与文件名一致,但为了统一,还是建议命名为HelloWorld.scala
。
(3)Scala语句结束没有;
。
1 | $ scala HelloWorld.scala //编译Scala文件 |
Scala语法(Scala 2.13.0)
变量与声明值
scala有两种类型的变量:
- val:不可变,在生命是就必须初始化,而且初始化以后就不能再赋值
- var:可变的,声明的时候需要进行初始化,初始化以后还可以再次进行赋值
Scala是自动推断变量的类型。也可以显式声明变量的类型。在每个程序中,scala都会自动添加java.lang._
的引用,这样就在所有原文件中引入了java.lang包内的所有东西。
1 | val myStr = "HelloWorld!" |
基本数据类型
Byte,Char,Short,Int,Long,Float,Double和Bollean。
注意在Scala中,这些类型都是Class,并且都是scala包的成员。
在Scala中没有提供++或–的操作符,当需要递增或递减时,只能使用i += 1
方法:
a 方法 b
a.方法(b)
Range
在执行for循环的时候,经常会用到数值序列,可以用Range来实现。Range可以支持创建不同数据类型的数值序列,包括Int,Long,Float,Double,Char,BigInt和BigDecimal等。
(1) 创建从1到5的数值序列
1 | scala> 1 to 5 |
(2) 创建从1到5的数值序列,不包含区间终点5, 步长为1
1 | scala> 1 until 5 |
(3) 步长为2
1 | scala> 1 to 10 by 2 |
同样上述也可以创建为float类型
读写文件
(1) 写入文本文件,执行后会在~/
下建立output.txt文件
1 | scala> import java.io.PrintWriter |
(2) 读取文件
1 | scala> import scala.io.Source |
控制结构与Java语言相同
- if
- while
- for(变量<-表达式) 语句块
变量<-表达式
被称为 生成器(generator)
1 | for(i <- 1 to 5 if i % 2 == 0) println(i) |
for还具有推导式。有时候需要对生成器过滤的结构进行进一步处理,我们可以使用yield关键字,对过滤后的结果构建一个集合.
1 | for(i <- 1 to 5 if i % 2==0) yield i |
数据结构
数组
1 | val intValueArr = new Array[Int](3) //声明一个长度为3的整形数组,每个数组元素初始化为0 |
列表
1 | scala> val intList = List(1,2,3) |
元组。
元组是不同类型的值的聚集。列表中的每个元素必须是相同类型。而元组可以包含不同类型的元素。
1 | scala> val tuple = ("BigData", 2015, 07, 1.2) |
集合 Set。
集合是不重复元素的集合。列表中的元素是按照插入的先后顺序来组织的,但集合中不会记录元素的插入顺序,而是以hash方法对元素的值进行组织。
1 | scala> val set = Set("a", "b") |
映射 Map
key->value键值对映射
1 | scala> val university = Map("XMU" -> "Xiamen University") |
上述定义的Map是无法更新映射中的元素,也无法增加新的元素。如果需要更新增加元素,需要定义一个可变的映射。
1 | scala> val university1 = Map("XMU"->"Xiamen University"); |
循环遍历映射,基本格式为:
1 | for ((k, v) <- map) 语句块 |
迭代器 Iterator
提供一种访问集合的方法。迭代器包含两个基本操作:next和hasNext。
1 | scala> val iter = Iterator("Hadoop", "Spark", "Scala") |
类 class 与Java的类相同
1 | class Counter{ |
在类方法的定义中使用def,上述Unit为返回值类型。在Scala中没有返回值就用Unit(Java中的void)。方法的返回值不需要return语句。方法里边最后一个表达式的值就是方法的返回值。
Scala类的主构造器。Scala的主构造器与Java的构造函数有明显不同,Scala的主构造器是整个类体,需要在类名称后边罗列出构造器所需的所有参数,这些参数被编译成字段,字段的值就是创建对象时传入的参数的值。
1 | class Counter(val name:String, val mode:Int){ |
对象 object
Scala没有提供Java的静态方法或静态字段,但是可以采用object关键字实现单例对象,具备和Java静态方法同样的功能。
- 单例对象,跟类定义很相似,只是用object来定义
1
2
3
4
5
6
7object Person{
private var lastId = 0 //一个人的身份编号
def newPersonId() = {
lastId += 1
lastId
}
} - 伴生对象,在Java中,我们经常需要用到同时包含实例方法和静态方法的类,在Scala中可以通过伴生对象来实现。 当单例对象与某个类具有相同的名称时 ,它被称为这个类的“伴生对象”。类和它的伴生对象必须存在同一个文件中,而且可以相互访问私有成员(字段和方法)。
1 | class Person{ |
应用程序对象。每个scala应用程序都必须从一个对象的main方法开始。
(1)如果代码中没有定义类,只有单例对象,那么可以不用编译或者在交互shell中直接用scala命令运行得到结果
(2)先用scalac命令对代码进行编译,然后用scala命令进行运行。apple方法和update方法。在scala中,用括号传递给变量(或对象)一个或多个参数时,scala会把他们转换成apply方法的调用;当对带有括号并包括一到若干参数的对象进行赋值时,编译器将调用对象的update方法。
继承
scala中的继承与Java的不同:
- 重写一个非抽象方法必须使用override修饰符
- 只有主构造器可以调用超类的主构造器
- 在子类中重写超类的抽象方法时,不需要使用overrider关键字
- 可以重写超类中的字段
Scala与Java一样不允许从多个超类继承。
抽象类
1 | abstract class Car{ |
要点:
- 定义一个抽象类,需要使用关键字abstract
- 定义一个抽象类的抽象方法,也不需要关键字abstract,只要把方法体空着,不写方法体就可以了(而在Java中,抽象方法时必须用abstract修饰的)
- 抽象类中定义的字段,只要没有给出初始化值,就表示是一个抽象字段,但是抽象字段必须要声明类型,不能省略类型否则会编译报错。
继承类
1 | class BMWCar extends Car{ |
特质 trait 对应于Java的接口
scala的trait不仅实现了接口的功能,还具备了其他的特性。scala的trait是代码重用的基本单元,可以同时拥有抽象方法和具体方法。与Java一样,一个类只能继承自一个超类,却可以实现多个trait。
跟Java的接口Interface一样,trait中的字段和方法都是默认抽象并且是public。但使用trait仍然是extends或者with。第一个实现trait用extends,之后的可以反复使用with关键字混入更多trait
1 | trait CarId{ |
trait中除了抽象字段和抽象方法,也可以包含具体实现,也就是说trait中的字段和方法不一定要是抽象的。
模式匹配
简单匹配
Java中有switch-case语句,但只能按顺序匹配简单的数据类型和表达式。在Scala中
1 | val colorNum = 1 |
类型匹配
scala可以对表达式的类型进行匹配
1 | for(elem <- List(9, 12.3, "Spark", "Hadoop", "Hello")){ |
守卫guard语句
可以在模式匹配中添加一些必要的处理逻辑。
1 | for(elem <- List(1,2,3,4)){ |
for表达式中的模式
1 | for((k, v) <- 映射) 语句块 |
case类的匹配
case类是一种特殊的类,经过优化被用于模式匹配
1 | case class Car(brand:String, price:Int) //case类,主构造器 |
Option类型
Option类型用case类来表示可能存在、也可能不存在的值。对于每种语言来说,都会有一个关键字来表示一个对象引用的是“无”,在Java中使用的是null。Scala融合了函数式编程风格,当预计变量或函数返回值可能不会引用任何值的时候,建议使用Option类型
1 | scala> var myMap = Map("hadoop"->5, "spark"->4, "car"->3) |
函数编程
函数式编程可以较好的满足分布式并行编程的需求(函数式编程的一个重要特性就是值不可变性,这对于编写可扩展的并发程序可以带来巨大好处,因为它避免了对公共的可变状态进行同步访问控制的复杂问题)。
函数字面量可以体现函数式编程的核心理念。字面量包括整数字面量,浮点数字面量,布尔型字面量,字符字面量,字符串字面量,符号字面量,函数字面量和元组字面量。
函数字面量 :在函数式编程中,函数可以像任何其他数据类型一样被传递和操作,也就是说函数的使用方式和其他数据类型的使用方式完全一致。由此就区分开了函数的“类型”和函数的“值”两个概念,而函数的“值”,就是“函数字面量”。
传统函数def counter(value:Int):Int = {value += 1}
上面这个传统函数的 类型 为(Int) => Int
上面这个函数的 值 为(value) => {value += 1}
那么我们可以按照如下方式定义上述传统函数val counter: (Int)=>Int = {(value) => value+=1}
这与scala中定义变量字面量的方式是一致的:val num: Int = 5
匿名函数:我们不需要给每个函数命名(num:Int) => num*2
这种匿名函数的表达方式为’lambda表达式‘。(参数) => 表达式
在lambda表达式中,至少需要在一处表明参数的类型,才可以推断参数类型
闭包:闭包是一个比较特殊的函数。反应一个从开放到封闭的过程。对于普通函数而言,函数中只会应用函数中定义的变量,不会引入函数外部的变量。而闭包会引用函数外部的变量。val addMore = (x:Int) => x+more
在这个函数定义中,引用了变量more。而more并没有在函数中定义,是一个函数外部的变量。如果现在去执行这条语句,编译器会因为more是一个自由变量还没有绑定值而报错。此时函数是开放的。因为为了使该lambda函数得到正常结果,必须在函数外部给出more的值。
1 | var more = 1 |
在给定more具体数值1之后,lambda表达式中的more变量也就绑定了具体的值1,不再是自由变量。这个lambda函数也就从开放状态转成了封闭状态。每次addMore函数被调用时都会创建一个新闭包,每个闭包都会访问闭包创建时活跃的more变量。
高阶函数
一个接受其他函数作为参数或者返回一个函数的函数就是高阶函数
例如,有一个函数对给定两个数区间内的所有整数求和:
1 | def sumInts(a:Int, b:Int):Int = { |
重新设计该函数的实现
1 | //定义一个新的函数sum,以函数f为参数 |
这其中sum函数的类型是(Int=>Int, Int, Int) => Int
接受了一个函数作为参数,因此是一个高阶函数。
高阶函数可以实现对同样的字段通过控制传入函数f来进行不同操作的类似操作,简化编写逻辑。
占位符语法
1 | scala> val numList = List(-3, -5, 1, 6, 9) |
可以看出x => x>0
与_ > 0
这两个函数字面量是等价的。当采用下划线表示方法时,对于列表numList中的每个元素都会依次传入用来替换下划线。
函数式编程中针对集合的操作
列表的遍历
1 | val list = List(1,2,3,4,5) |
或者使用foreach进行遍历
1 | list.foreach(elem => println(elem)) |
映射的遍历
1 | for((k, v) <- 映射) 语句块 |
map操作和flatMap操作
map操作是针对集合的典型变换操作,它将 某个函数 应用到集合中的 每个元素 ,并产生一个 结果集合 。
1 | val books = List("Hadoop", "Hive", "HDFS") |
对所有的s,都执行s.toUpperCase操作
flatMap是对map的一种扩展。在flatMap中,我们会传入一个函数,该函数对每个输入都会返回 一个集合 ,然后flatMap会把生成的多个集合“拍扁”成为一个集合。
1 | val list = List("Hadoop", "Spark") |
filter操作
1 | val university = Map("XMU" -> "Xiamen University", "THU" -> "Tsinghua University") |
Reduce操作
在Scala中,使用reduce对集合中的元素进行规约,他是一个二元操作。
reduce包含reduceLeft和reduceRight两种操作,前者从集合的头部开始操作,后者从集合的尾部开始操作。默认reduce操作是reduceLeft
1 | val list = List(1,2,3,4,5) |
fold操作
fold操作与reduce操作比较类似。fold操作需要从一个初始的”种子“值开始,并以该值作为上下文,处理集合中的每一个元素。
1 | val list = List(1,2,3,4,5) |
fold需要提供一个初始值,首先第一个操作时初始值与第一个遍历参数进行计算。同样提供了foldLeft和foldRight操作,默认fold操作时从左到右。