Scala语言学习汇总

参考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
2
3
4
5
object HelloWorld{
def main(args: Array[String]){
println("Hello World!")
}
}

(1)Scala程序中同样要定义程序入口main()函数。但与Java不同的是,在Java中,main函数必须是静态函数 public static void main(String[] args),而Scala中必须使用对象方法。
(2)在Scala中对象的命名不一定要与文件名一致,但为了统一,还是建议命名为HelloWorld.scala
(3)Scala语句结束没有;

1
2
$ scala HelloWorld.scala //编译Scala文件
$ scala -classpath . HelloWorld //执行生成的HelloWorld对象名称

Scala语法(Scala 2.13.0)

变量与声明值

scala有两种类型的变量:

  • val:不可变,在生命是就必须初始化,而且初始化以后就不能再赋值
  • var:可变的,声明的时候需要进行初始化,初始化以后还可以再次进行赋值

Scala是自动推断变量的类型。也可以显式声明变量的类型。在每个程序中,scala都会自动添加java.lang._的引用,这样就在所有原文件中引入了java.lang包内的所有东西。

1
2
val myStr = "HelloWorld!"
val myStr2 : String = "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
2
3
4
5
scala> 1 to 5
res1: scala.collection.immutable.Range.Inclusive = Range 1 to 5

scala> 1.to(5)
res2: scala.collection.immutable.Range.Inclusive = Range 1 to 5

(2) 创建从1到5的数值序列,不包含区间终点5, 步长为1

1
2
scala> 1 until 5
res3: scala.collection.immutable.Range = Range 1 until 5

(3) 步长为2

1
2
scala> 1 to 10 by 2
res6: scala.collection.immutable.Range = inexact Range 1 to 10 by 2

同样上述也可以创建为float类型

读写文件

(1) 写入文本文件,执行后会在~/下建立output.txt文件

1
2
3
4
5
6
7
8
9
scala> import java.io.PrintWriter
import java.io.PrintWriter

scala> val out = new PrintWriter("output.txt")
out: java.io.PrintWriter = java.io.PrintWriter@52035328

scala> for(i <- 1 to 5) out.println(i)

scala> out.close()

(2) 读取文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scala> import scala.io.Source
import scala.io.Source

scala> val inputFile = Source.fromFile("output.txt")
inputFile: scala.io.BufferedSource = <iterator>

scala> val lines = inputFile.getLines
lines: Iterator[String] = <iterator>

scala> for(line <- lines) println(line)
1
2
3
4
5

控制结构与Java语言相同

  • if
  • while
  • for(变量<-表达式) 语句块

变量<-表达式被称为 生成器(generator)

1
2
for(i <- 1 to 5 if i % 2 == 0) println(i)
for(i <- 1 to 5; j <- 1 to 3) println(i*j)

for还具有推导式。有时候需要对生成器过滤的结构进行进一步处理,我们可以使用yield关键字,对过滤后的结果构建一个集合.

1
for(i <- 1 to 5 if i % 2==0) yield i

数据结构

数组

1
2
3
val intValueArr = new Array[Int](3) //声明一个长度为3的整形数组,每个数组元素初始化为0
intValueArr(0) = 12
val intValueArr = Array(12, 45, 33) //简洁初始化数组

列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scala> val intList = List(1,2,3)
intList: List[Int] = List(1, 2, 3)

scala> intList.head
res10: Int = 1

scala> intList.tail
res11: List[Int] = List(2, 3)

scala> 0::intList //使用::在列表的头部增加新的元素,得到一个新的列表。注意::是右结合的
res12: List[Int] = List(0, 1, 2, 3)

scala> intList.sum
res16: Int = 6

元组。

元组是不同类型的值的聚集。列表中的每个元素必须是相同类型。而元组可以包含不同类型的元素。

1
2
3
4
5
scala> val tuple = ("BigData", 2015, 07, 1.2)
tuple: (String, Int, Int, Double) = (BigData,2015,7,1.2)

scala> tuple._1 //访问第一个元素
res17: String = BigData

集合 Set。

集合是不重复元素的集合。列表中的元素是按照插入的先后顺序来组织的,但集合中不会记录元素的插入顺序,而是以hash方法对元素的值进行组织。

1
2
scala> val set = Set("a", "b")
set: scala.collection.immutable.Set[String] = Set(a, b)

映射 Map

key->value键值对映射

1
2
3
4
5
scala> val university = Map("XMU" -> "Xiamen University")
university: scala.collection.immutable.Map[String,String] = Map(XMU -> Xiamen University)

scala> university("XMU")
res19: String = Xiamen University

上述定义的Map是无法更新映射中的元素,也无法增加新的元素。如果需要更新增加元素,需要定义一个可变的映射。

1
2
3
4
5
scala> val university1 = Map("XMU"->"Xiamen University");
university1: scala.collection.mutable.Map[String,String] = HashMap(XMU -> Xiamen University)

scala> university1 += ("THU" -> "Tsinghua University")
res22: university1.type = HashMap(THU -> Tsinghua University, XMU -> Xiamen University)

循环遍历映射,基本格式为:

1
for ((k, v) <- map) 语句块

迭代器 Iterator

提供一种访问集合的方法。迭代器包含两个基本操作:next和hasNext。

1
2
3
4
5
6
7
8
scala> val iter = Iterator("Hadoop", "Spark", "Scala")
iter: Iterator[String] = <iterator>

scala> while(iter.hasNext)
| println(iter.next())
Hadoop
Spark
Scala

类 class 与Java的类相同

1
2
3
4
5
6
7
8
class Counter{
private var value = 0;
def increment(): Unit = {value += 1}
def current(): Int = {value}
}

val myCounter = new Counter;
myCounter.increment();

在类方法的定义中使用def,上述Unit为返回值类型。在Scala中没有返回值就用Unit(Java中的void)。方法的返回值不需要return语句。方法里边最后一个表达式的值就是方法的返回值。

Scala类的主构造器。Scala的主构造器与Java的构造函数有明显不同,Scala的主构造器是整个类体,需要在类名称后边罗列出构造器所需的所有参数,这些参数被编译成字段,字段的值就是创建对象时传入的参数的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Counter(val name:String, val mode:Int){
private var value = 0;
def increment(step:Int):Unit = {value += step}
def current():Int = {value}
def info():Unit = {printf("Name:%s and mode is %d\n", name, mode)}
}
object MyCounter{
def main(args:Array[String]){
val myCounter = new Counter("Timer", 2);
myCounter.info //显示计数器信息
myCounter.increment(2) //添加步长
printf("Current Value is: %d\n", myCounter.current) //显示计数器当前值
}
}

对象 object

Scala没有提供Java的静态方法或静态字段,但是可以采用object关键字实现单例对象,具备和Java静态方法同样的功能。

  • 单例对象,跟类定义很相似,只是用object来定义
    1
    2
    3
    4
    5
    6
    7
    object Person{
    private var lastId = 0 //一个人的身份编号
    def newPersonId() = {
    lastId += 1
    lastId
    }
    }
  • 伴生对象,在Java中,我们经常需要用到同时包含实例方法和静态方法的类,在Scala中可以通过伴生对象来实现。 当单例对象与某个类具有相同的名称时 ,它被称为这个类的“伴生对象”。类和它的伴生对象必须存在同一个文件中,而且可以相互访问私有成员(字段和方法)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person{
private val id = Person.newPersonId() //调用伴生对象的方法
private var name = ""
def this(name:String){ //第一辅助构造器
this() //主构造器
this.name = name //辅助构造器构造类字段
}
def info(){
printf("The id of %s is %d.\n", name, id)
}
}

object Person{
private var lastId = 0
private def newPersonId() = { //它承担了一个相当于Java中static方法的任务
lastId += 1
lastId
}
def main(args:Array[String]){
val person1 = new Person("Ziyu")
val person2 = new Person("Minxing")
person1.info()
person2.info()
}
}
  • 应用程序对象。每个scala应用程序都必须从一个对象的main方法开始。
    (1)如果代码中没有定义类,只有单例对象,那么可以不用编译或者在交互shell中直接用scala命令运行得到结果
    (2)先用scalac命令对代码进行编译,然后用scala命令进行运行。

  • apple方法和update方法。在scala中,用括号传递给变量(或对象)一个或多个参数时,scala会把他们转换成apply方法的调用;当对带有括号并包括一到若干参数的对象进行赋值时,编译器将调用对象的update方法。

继承

scala中的继承与Java的不同:

  • 重写一个非抽象方法必须使用override修饰符
  • 只有主构造器可以调用超类的主构造器
  • 在子类中重写超类的抽象方法时,不需要使用overrider关键字
  • 可以重写超类中的字段

Scala与Java一样不允许从多个超类继承。

抽象类

1
2
3
4
5
6
7
abstract class Car{
val carBradn:String
def info()
def greeting(){
println("Welcome to my car!")
}
}

要点:

  • 定义一个抽象类,需要使用关键字abstract
  • 定义一个抽象类的抽象方法,也不需要关键字abstract,只要把方法体空着,不写方法体就可以了(而在Java中,抽象方法时必须用abstract修饰的)
  • 抽象类中定义的字段,只要没有给出初始化值,就表示是一个抽象字段,但是抽象字段必须要声明类型,不能省略类型否则会编译报错。

继承类

1
2
3
4
5
6
7
8
9
class BMWCar extends Car{
override val carBrand = "BMW" //重写抽象字段必须加override
override def info(){
printf("This is a %s car. It is on sale.", carBrand)
} //重写超类抽象方法可以不用加override,加了也不会报错
overrider def greeting(){
println("Welcome to my BMW car!")
} //重写超类非抽象方法必须加override
}

特质 trait 对应于Java的接口

scala的trait不仅实现了接口的功能,还具备了其他的特性。scala的trait是代码重用的基本单元,可以同时拥有抽象方法和具体方法。与Java一样,一个类只能继承自一个超类,却可以实现多个trait。

跟Java的接口Interface一样,trait中的字段和方法都是默认抽象并且是public。但使用trait仍然是extends或者with。第一个实现trait用extends,之后的可以反复使用with关键字混入更多trait

1
2
3
4
5
6
7
8
9
10
11
12
13
trait CarId{
var id:Int
def currentId():Int
}

trait CarGreeting{
def greeting(msg:String) {println(msg)}
}

class BMWCarId extends CarId with CarGreeting{
override var id = 20000
def currentId():Int = {id +=1; id}
}

trait中除了抽象字段和抽象方法,也可以包含具体实现,也就是说trait中的字段和方法不一定要是抽象的。

模式匹配

简单匹配
Java中有switch-case语句,但只能按顺序匹配简单的数据类型和表达式。在Scala中

1
2
3
4
5
6
7
8
9
val colorNum = 1
val colorStr = colorNum match{
case 1 => "red"
case 2 => "green"
case 3 => "yellow"
case _ => "Not Allowed"
//也可以使用
case unexpected => unexpected + "is Not Allowed"
}

类型匹配
scala可以对表达式的类型进行匹配

1
2
3
4
5
6
7
8
9
for(elem <- List(9, 12.3, "Spark", "Hadoop", "Hello")){
val str = elem match{
case i:Int => i + "is an int value"
case d:Double => d + "is a double value"
case "Spark" => "Spark is found"
case s:String => s + "is a string value"
case _ => "This is an unexpected value"
}
}

守卫guard语句
可以在模式匹配中添加一些必要的处理逻辑。

1
2
3
4
5
6
for(elem <- List(1,2,3,4)){
elem match{
case_ if(elem%2 == 0) => println(elem + "is even.")
case_ => println(elem + "is odd.")
}
}

for表达式中的模式

1
for((k, v) <- 映射) 语句块

case类的匹配
case类是一种特殊的类,经过优化被用于模式匹配

1
2
3
4
5
6
7
8
case class Car(brand:String, price:Int) //case类,主构造器
val myBMWCar = new Car("BMW", 1200000)
for (car <- List(myBMWCar)){
car match{
case Car("BMW", 1200000) => println("Hello, BMW!")
case Car(brand, price) => println("Brand:" + brand + ", Price:" + price + ", do you want it?" )
}
}

Option类型
Option类型用case类来表示可能存在、也可能不存在的值。对于每种语言来说,都会有一个关键字来表示一个对象引用的是“无”,在Java中使用的是null。Scala融合了函数式编程风格,当预计变量或函数返回值可能不会引用任何值的时候,建议使用Option类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scala> var myMap = Map("hadoop"->5, "spark"->4, "car"->3)
myMap: scala.collection.immutable.Map[String,Int] = Map(hadoop -> 5, spark -> 4, car -> 3)

scala> myMap.get("hadoop")
res1: Option[Int] = Some(5)

scala> myMap.get("hive")
res2: Option[Int] = None

val found = myMap.get("hadoop")
found.get //返回Some中的数据

val found = myMap.get("hive")
found.getOrElse("Not Found") //会返回字符串

函数编程

函数式编程可以较好的满足分布式并行编程的需求(函数式编程的一个重要特性就是值不可变性,这对于编写可扩展的并发程序可以带来巨大好处,因为它避免了对公共的可变状态进行同步访问控制的复杂问题)。
函数字面量可以体现函数式编程的核心理念。字面量包括整数字面量,浮点数字面量,布尔型字面量,字符字面量,字符串字面量,符号字面量,函数字面量和元组字面量。

函数字面量 :在函数式编程中,函数可以像任何其他数据类型一样被传递和操作,也就是说函数的使用方式和其他数据类型的使用方式完全一致。由此就区分开了函数的“类型”和函数的“值”两个概念,而函数的“值”,就是“函数字面量”。

传统函数
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
2
var more = 1
val addMore = (x:Int) => x+more

在给定more具体数值1之后,lambda表达式中的more变量也就绑定了具体的值1,不再是自由变量。这个lambda函数也就从开放状态转成了封闭状态。每次addMore函数被调用时都会创建一个新闭包,每个闭包都会访问闭包创建时活跃的more变量。

高阶函数
一个接受其他函数作为参数或者返回一个函数的函数就是高阶函数
例如,有一个函数对给定两个数区间内的所有整数求和:

1
2
3
def sumInts(a:Int, b:Int):Int = {
if(a > b) 0 else a+sumInts(a+1, b)
}

重新设计该函数的实现

1
2
3
4
5
6
7
8
//定义一个新的函数sum,以函数f为参数
def sum(f:Int => Int, a:Int, b:Int):Int = {
if(a>b) 0 else f(a) + sum(f, a+1, b)
}
//定义一个新的函数self,该函数出入一个整数x,然后直接输出x本身
def self(x:Int): Int = x
//重新定义sumInts函数
def sumInts(a:Int, b:Int): Int = sum(self, a, b)

这其中sum函数的类型是(Int=>Int, Int, Int) => Int 接受了一个函数作为参数,因此是一个高阶函数。

高阶函数可以实现对同样的字段通过控制传入函数f来进行不同操作的类似操作,简化编写逻辑。

占位符语法

1
2
3
4
5
6
7
8
scala> val numList = List(-3, -5, 1, 6, 9)
numList: List[Int] = List(-3, -5, 1, 6, 9)

scala> numList.filter(x => x>0)
res10: List[Int] = List(1, 6, 9)

scala> numList.filter(_ >0)
res11: List[Int] = List(1, 6, 9)

可以看出x => x>0_ > 0这两个函数字面量是等价的。当采用下划线表示方法时,对于列表numList中的每个元素都会依次传入用来替换下划线。

函数式编程中针对集合的操作

列表的遍历

1
2
val list = List(1,2,3,4,5)
for(elem <- list) println(elem)

或者使用foreach进行遍历

1
list.foreach(elem => println(elem))

映射的遍历

1
for((k, v) <- 映射) 语句块

map操作和flatMap操作

map操作是针对集合的典型变换操作,它将 某个函数 应用到集合中的 每个元素 ,并产生一个 结果集合

1
2
val books = List("Hadoop", "Hive", "HDFS")
books.map(s => s.toUpperCase)

对所有的s,都执行s.toUpperCase操作

flatMap是对map的一种扩展。在flatMap中,我们会传入一个函数,该函数对每个输入都会返回 一个集合 ,然后flatMap会把生成的多个集合“拍扁”成为一个集合。

1
2
3
4
5
val list = List("Hadoop", "Spark")
list: List[String] = List(Hadoop, Spark)

scala> list.flatMap(s => s.toUpperCase)
res15: List[Char] = List(H, A, D, O, O, P, S, P, A, R, K)

filter操作

1
2
val university = Map("XMU" -> "Xiamen University", "THU" -> "Tsinghua University")
val universityOfXiamen = university filter{kv => kv._2 contains "Xiamen"}

Reduce操作

在Scala中,使用reduce对集合中的元素进行规约,他是一个二元操作。
reduce包含reduceLeft和reduceRight两种操作,前者从集合的头部开始操作,后者从集合的尾部开始操作。默认reduce操作是reduceLeft

1
2
3
val list = List(1,2,3,4,5)
list.reduceLeft( _ + _ ) //从列表头部开始,对两两元素进行求和操作,下划线是占位符用来获取当前的两个元素。
list.reduceRight( _ - _ )

fold操作

fold操作与reduce操作比较类似。fold操作需要从一个初始的”种子“值开始,并以该值作为上下文,处理集合中的每一个元素。

1
2
val list = List(1,2,3,4,5)
list.fold(10)(_*_)

fold需要提供一个初始值,首先第一个操作时初始值与第一个遍历参数进行计算。同样提供了foldLeft和foldRight操作,默认fold操作时从左到右。