0%

Scala

Scala是一种多范式的编程语言,其设计的初衷是要集成面向对象编程和函数式编程的各种特性。Scala运行于Java平台(Java虚拟机),并兼容现有的Java程序。

  1. Spark是新一代内存级大数据计算框架,是大数据的重要内容。
  2. Spark就是使用Scala编写的。因此为了更好的学习Spark, 需要掌握Scala这门语言。

概述

Scala 是一门静态类型语言,支持混合范式,语法简洁、优雅、灵活,运行在 JVM 之上。Scala 拥有一套复杂的类型系统,Scala 方言既能用于编写简短的解释脚本,也能用于构建大型复杂系统。

  • 运行在 JVM 之上的语言
    Scala 不仅利用了 JVM 的高性能以及最优化性,Java 丰富的工具及类库生态系统也为其所用。
  • 静态类型
    在 Scala 语言中,静态类型(static typing)是构建健壮应用系统的一个工具。Scala 修正了 Java 类型系统中的一些缺陷,此外通过类型推演(type inference)也免除了大量的冗余代码。
  • 混合式编程范式——面向对象编程
    Scala 完全支持面向对象编程(OOP)。Scala 引入特征(trait)改进了 Java 的对象模型。trait 能通过使用混合结构(mixin composition)简洁地实现新的类型。在 Scala 中,一切都是对象,即使是数值类型。
  • 混合式编程范式——函数式编程
    Scala 完全支持函数式编程(FP),函数式编程已经被视为解决并发、大数据以及代码正确性问题的最佳工具。使用不可变值、被视为一等公民的函数、无副作用的函数、高阶函数以及函数集合,有助于编写出简洁、强大而又正确的代码。
  • 复杂的类型系统
    Scala 对 Java 类型系统进行了扩展,提供了更灵活的泛型以及一些有助于提高代码正确性的改进。通过使用类型推演,Scala 编写的代码能够和动态类型语言编写的代码一样精简。
  • 简洁、优雅、灵活的语法
    使用 Scala 之后,Java 中冗长的表达式不见了,取而代之的是简洁的 Scala 方言。Scala提供了一些工具,这些工具可用于构建领域特定语言(DSL),以及对用户友好的 API接口。
  • 可扩展的架构
    使用 Scala,你能编写出简短的解释性脚本,并将其粘合成大型的分布式应用。以下四种语言机制有助于提升系统的扩展性:1) 使用 trait 实现的混合结构;2) 抽象类型成员和泛型;3) 嵌套类;4) 显式自类型(self type)。

安装

下载 Scala for Windows

下载 Scala for Linux

注:需要提前安装好 JDK。

Scala基础

Scala 源文件的扩展名为 .scala,程序的执行入口是 main(),严格区分大小写,每行语句的最后不需要加分号,如果同一行有多条语句,除了最后一条外其他语句后需要加分号。

分号

分号是表达式之间的分隔符,但可以通过推断得出,因此一般省略分号。当一行结束时,Scala 就认为表达式结束了,除非它可以推断出该表达式尚未结束,需要延续到下一行,如下面这些例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//末尾的等号表明下一行还有未结束的代码
def equalsign(s: String) =
println("equalsign: " + s)

//末尾的大括号表明下一行还有未结束的代码
def equalsign2(s: String) = {
println("equalsign2: " + s)
}

//末尾的逗号、句号和操作符都可以表明,下一行还有未结束的代码
def commas(s1: String,
s2: String) = Console.
println("comma: " + s1 +
", " + s2)

与编译器相比,REPL(读取 Read - 求值 Eval - 打印 Print - 循环 Loop,即“交互式解释器”)更容易将每行视为单独的表达式。因此,在 REPL 中输入跨越多行的表达式时,最安全的做法是每行都以上述脚本中出现过的符号结尾。如果将多个表达式放在同一行中,表达式之间用分号隔开。

变量

在声明变量时,Scala 允许用 val 来声明不可变(只读)的变量,或用 var 来声明可变的(读写)变量。语法如下:

1
val/var 变量名 [:变量类型] = 变量值

如果在交互式 REPL 中没有定义变量名,那么 REPL会自动分配一个变量名:

REPL自动分配变量名

注意:

  • Scala 中不指定的变量类型,会自动推断给出。
  • 变量在声明的时候必须被初始化。存在少数例外情况,例如用在构造函数的参数中。
  • 为了减少可变性引起的问题,应该尽可能地使用不可变变量。

块表达式

块表达式是指定义变量时用大括号 {} 包裹一系列表达式,块中最后一个表达式的值就是变量的值。

1
2
3
4
5
6
var block = {
println("Hi SANNAHA")
val a = 30
val b = 20
a - b
}

注意,此时块表达式中的 println() 会根据定义顺序执行,而非调用变量时才执行。验证如下:

1
2
3
4
5
6
7
8
9
10
11
12
object FirstDemo {
def main(args: Array[String]): Unit = {
var block = {
println("Hi SANNAHA");
val a = 30;
val b = 20;
a - b
}
println("Hello Scala")
println(block)
}
}

FirstDemo

数据类型

  • Scala 尽量使 Java 中所谓的基本类型(即 CharByteShortIntLongFloatDoubleBoolean)面向对象特性更加一致,因此这些类型在 Scala 中是包含有方
    法的对象,不再有基本数据类型的概念,统一作为对象使用。而在编译时会将这些类型尽可能地转为基本类型,以得到基本类型的运行效率。
  • 在 Scala 中,String 直接引用的是 java.lang.String 这个 Java 当中的类型,因此不需要任何额外的转换,同样可以使用其包含的方法。
  • 每一种数据类型都有对应的 Rich* 类型,如 RichIntRichChar 等,为基本类型提供了更多实用的操作。

Scala数据类型

统一类型,是 Scala 的又一大特点。Scala 中所有的类都继承自根类型 Any,Scala 中还定义了几个底层类(Bottom Class),如 NullNothing

  • Any:是其他所有类的超类。
  • AnyRef:是所有引用类型(reference class)的基类。
  • Unit:用来标识过程,也就是没有明确返回值的函数,类似于 Java 里的 void。Unit 只有一个实例 (),这个实例也没有实质的意义。
  • Null:是所有引用类型的子类,只有一个实例对象 nullnull 可以赋值给任意引用类型,但不能赋值给值类型。
  • Nothing:是其他所有类型的子类。可以赋值给其他任何类型,用于异常,表明不正常的返回。

操作符重载

Scala 中的操作符(+-*/%)都是方法,既可以像 Java 中的一样使用,也可以像调用方法一样使用。Scala 中没有 ++-- 操作符,可以通过 +=-= 来实现相同的效果。

Scala运算符重载

惰性赋值

如果希望以延迟的方式初始化某值,并且表达式不会被重复计算,则需要使用惰性赋值。下面列举了一些需要用到该技术的常见场景:

  • 表达式的执行代价昂贵(比如打开数据库连接),希望能推迟该操作,直到确实需要时才执行它。
  • 为了缩短模块的启动时间,可以将当前不需要的某些工作推迟执行。
  • 为了确保对象中其他的字段的初始化过程能优先执行,需要将某些字段惰性化。

使用 lazy 修饰 val,推迟初始化时机:

1
2
3
4
5
6
7
8
9
10
11
12
object ExpensiveResource {
def main(args: Array[String]): Unit = {
def init(): String = {
println("打开数据库连接")
"完成赋值"
}

lazy val msg = init()
println("执行其他操作")
println(msg)
}
}
惰性赋值

正常赋值:

1
2
3
4
5
6
7
8
9
10
11
12
object ExpensiveResource {
def main(args: Array[String]): Unit = {
def init(): String = {
println("打开数据库连接")
"完成赋值"
}

val msg = init()
println("执行其他操作")
println(msg)
}
}
正常赋值

惰性赋值与方法调用有哪些差别呢?

对于方法调用而言,每次调用方法时方法体都会被执行;而惰性赋值则不然,首次使用该值时,用于初始化的“代码体”才会被执行一次。这种只能执行一次的计算对于可变字段而言几乎没有任何意义。因此, lazy 关键字并不能用于修饰 var 变量。

我们通过保护式(guard)来实现惰性值。当客户代码引用了惰性值时,保护式会拦截引用并检查此时是否需要初始化惰性。由于保护式能确保惰性值在第一次访问之前便已初始化,因此增加保护式检查只有当第一次引用惰性值时才是必要的。但不幸的是,很难解除之后的惰性值保护式检查。所以,与“立刻”值相比,惰性值具有额外的开销。因此只有当保护式带来的额外开销小于初始化带来的开销时,或者将某些值惰性化能简化系统初始化过程并确保执行顺序满足依赖条件时,你才应该使用惰性值。

小结

万能的 _

  • 导包时代表所有的类
  • 代替所有的变量

  • Null:引用类型
  • Nothing:方法没有正常返回
  • Unit:方法的空返回值
  • Nil:表示空的 List
  • None:Option 中没有值

算子

  • Map:做映射,取出集合中每一条数据进行操作,有返回值
  • Foreach:取出每一条数据进行操作,没有返回值
  • ReduceLeft:折叠,从左到右进行折叠,需要两个参数:x(初始值或累计值),y(新值)
  • ReduceRight:折叠,从右到左进行折叠,需要两个参数:x(初始值或累计值),y(新值)
  • Fold/FoldLeft:折叠,从左到右进行折叠,需要三个参数:10(初始值),x(累计值),y(新值)
  • FoldRight:折叠,从右到左进行折叠,需要三个参数:10(初始值),x(累计值),y(新值)
  • Filter:过滤,传递返回值为 boolean 类型的匿名函数,保留结果为 true 的值

流程控制

if else

  • 从表面上看,Scala 的 if 语句看起来很像 Java 中的 if 语句。执行 if 语句时先对 if 条件表达式进行计算。假如表达式结果为 true,那么将执行对应的代码块。反之,将测试下一条件分支,以此类推。
  • 与 Java 中不同的是,Scala 中的 if 语句和几乎所有的其他语句都是具有返回值的表达式,因此能将 if 表达式的结果值赋给其他变量。所以 Java 中的三目运算符对于 Scala 来说是多余的, 因此 Scala 并不支持三目运算符。
  • if 语句返回值的类型也被称为所有条件分支的最小上界类型,也就是与每条子句可能的返回值类型最接近的父类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
object IfDemo {
def main(args: Array[String]): Unit = {
//if 和 else 的返回值类型一样,返回值为同样的类型
val a = 20
val b: String = if (a > 10) "大于10" else "小于或等于10"
println(b)

//if 和 else 的返回值类型不一样,返回二者的超类(Any 类型)
val age = 24
val result1: Any = if (age >= 18) "成年" else 0
println(result1)

//如果任何判断条件都不符合,返回 Unit 类型,该类型只有一个实例“()”,相当于 Java 中的 void
val result2: Unit = if (age < 18) "未成年"
println(result2)
}
}

IfDemo

while

Scala提供了类似于 Java 的 while 循环和 do while 循环,只要判断条件成立,将一直运行对应代码块。

注意:while 循环的返回值是没有意义的,也就是说 while 语句的返回值是 Unit 类型的 ()

1
2
3
4
5
6
7
8
9
10
object WhileLoop {
def main(args: Array[String]): Unit = {
var a = 1;
val b = while (a < 10) {
a += 1
}
println(a)
println(b)
}
}

WhileLoop

Scala 没有提供类似 Java 的 continuebreak 操作,Scala 提供的其他特性使得这两个功能没有存在的必要。你可以使用条件表达式或者使用递归判断循环是否应该继续。如果确实想要终止循环,可以使用 scala.util.control.Breaks 提供的 break() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object BreakLoop {
def main(args: Array[String]): Unit = {
var a = 1
val loop = new Breaks
loop.breakable {
while (a < 10) {
a += 1
if (a == 5) {
loop.break()
}
}
}
println(a)
}
}

BreakLoop

for推导式

Scala 为 for 循环这一常见的控制结构提供了非常多的特性,这些 for 循环的特性被称为 for 推导式(for comprehension)或 for 表达式(for expression)。

注:推导式一词起源于函数式编程。它表达了这样一个理念:我们遍历一个或多个集合,对集合中的元素进行“推导”,并从中计算出新的事物,新推导出的事物往往是另一个集合。

for循环

for 循环是基本的 for 推导式,这与 Java 中的 for 循环较为类似。 for 循环的返回值是 Unit 类型的 ()

基于列表 dogBreeds 中的每一个元素,创建临时变量 breedbreed 的值与元素值相同,之后打印 breed

1
2
3
4
5
6
7
8
9
object BasicForLoop {
def main(args: Array[String]): Unit = {
val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund",
"Scottish Terrier", "Great Dane", "Portuguese Water Dog")
val a = for (breed <- dogBreeds)
println(breed)
println(a)
}
}

BasicForLoop

生成器表达式

breed <- dogBreeds 这样的表达式也被称为生成器表达式(generator expression),生成器表达式之所以叫这个名字,是因为该表达式会基于集合生成单独的数值。左箭头操作符 <- 用于遍历像列表这样的集合。

可以使用生成器表达式对某些区间进行访问,以这种方式编写出的 for 循环更加自然:

  1. 访问使用 to 实现的左右均闭合集合。
1
2
3
4
5
6
7
object GeneratorForLoop {
def main(args: Array[String]): Unit = {
for(i <- 1 to 3){
println(i)
}
}
}

GeneratorForLoop1

  1. 访问使用 util 实现的前闭后开的集合。
1
2
3
4
5
6
7
object GeneratorForLoop {
def main(args: Array[String]): Unit = {
for(i <- 1 until 3){
println(i)
}
}
}

GeneratorForLoop2

  1. 支持循环嵌套。
1
2
3
4
5
6
7
object GeneratorForLoop {
def main(args: Array[String]): Unit = {
for (i <- 1 to 3; j <- 4 to 5) {
println(i + j)
}
}
}

GeneratorForLoop3

保护式

在for循环条件里面加入 if 表达式,来筛选出我们希望保留的元素,如果满足则进入for循环,如果不满足则不进入for循环。这些表达式也被称为保护式(guard)。

1
2
3
4
5
6
7
8
9
10
object GuardForLoop {
def main(args: Array[String]): Unit = {
val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund",
"Scottish Terrier", "Great Dane", "Portuguese Water Dog")
for (breed <- dogBreeds
if breed.contains("Terrier")
)
println(breed)
}
}

GuardForLoop1

可以在 for 循环中添加多个保护式,这里提供两种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object GuardForLoop {
def main(args: Array[String]): Unit = {
val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund",
"Scottish Terrier", "Great Dane", "Portuguese Water Dog")
for (breed <- dogBreeds
if breed.contains("Terrier")
if !breed.startsWith("Yorkshire")
)
println(breed)
for (breed <- dogBreeds
if breed.contains("Terrier") && !breed.startsWith("Yorkshire")
)
println(breed)
}
}

GuardForLoop2

Yielding

使用 yield 关键字能在 for 表达式中生成新的集合,用于接收遍历过程处理的结果。此时,可以使用大括号 for {} 代替小括号 for (),把参数列表封装在大括号中,可以使块结构的格式看起来更为直观。

每次执行 for 表达式时,过滤后的结果将生成 breed 值。随着代码的执行,这些结果值逐渐积累起来,累计而成的结果值集合被赋给了 filteredBreeds 对象。for-yield 表达式所生成的集合类型根据被遍历的集合类型推导得出。在下面的例子中,由于 filteredBreeds 源 于 dogBreeds 列 表, 而 dogBreeds 类 型 为 List[String] , 因此 filteredBreeds 的类型也为 List[String]

1
2
3
4
5
6
7
8
9
10
11
object YieldingForLoop {
def main(args: Array[String]): Unit = {
val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund",
"Scottish Terrier", "Great Dane", "Portuguese Water Dog")
val filteredBreeds = for {
breed <- dogBreeds
if breed.contains("Terrier")
} yield breed
println(filteredBreeds)
}
}

YieldingForLoop

注:for 推导式有一个不成文的约定:当 for 推导式仅包含单一表达式时使用小括号 (),当其包含多个表达式时使用大括号 {}。值得注意的是,使用小括号时,早前版本的 Scala 要求表达式之间必须使用分号。

其他特征

Scala 的 for 推导式还有一个有用的特征:你能够在 for 表达式中的最初部分定义值,并可以在后面的表达式中使用该值。如下所示:

1
2
3
4
5
6
7
8
9
10
11
object ScopedForLoop {
def main(args: Array[String]): Unit = {
val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund",
"Scottish Terrier", "Great Dane", "Portuguese Water Dog")
for {
breed <- dogBreeds
upcasedBreed = breed.toUpperCase()
}
println(upcasedBreed)
}
}

ScopedForLoop

需要注意的是,尽管 upcasedBreed 的值不可变,但并不需要使用 val 关键字进行限定。

什么时候使用左箭头 `<-`,什么时候该使用等号 `=` 呢?

1. for 推导式的第一句表达式必须使用箭头 `<-` 执行抽取 / 迭代操作。
  1. 遍历某一集合或其他像 Option 这样的容器并试图提取值时,应该使用箭头 <- 。执行并不需要迭代的赋值操作时,应使用等号 =

方法和函数

在 Scala 中,函数与方法是两个不同的概念,函数是一等公民。Scala 是一门函数式的编程语言,同时兼顾了面向对象语言的特性。

定义方法

Scala 定义方法的标准格式:

1
def 方法名(参数名1: 参数类型1, 参数名2: 参数类型2) : 返回类型 = {方法体}

示例

  1. 定义一个方法,同时定义方法的返回值类型为 Int
1
2
3
def fun1(a: String, b: Int): Int = {
b
}
  1. 定义一个方法,但不定义返回值的类型,返回值类型会通过自动推断给出:
1
2
3
def fun2(a: String, b: Int) = {
a
}
  1. 定义一个方法,不定义返回值,可以通过自动推断,返回不同类型的值
1
2
3
4
5
6
7
8
9
def fun3(first:Int,second:String) ={
if(first > 10){
first
}else{
second
}
}
val fun3Result = fun3(5,"helloworld")
println(fun3Result)
  1. 定义一个方法,参数给定默认值,如果不传入参数,就使用默认值来代替
1
2
3
4
// 在调用方法时可以通过参数名来指定我们的参数的值hello4(second="helloworld")
def fun4(first:Int = 10,second:String)={
println(first+"\t"+ second)
}
  1. 变长参数,方法的参数个数不定的,类似于java当中的方法的…可变参数
1
2
3
4
5
6
7
8
9
def fun5(first:Int*)={
var result = 0;
for(arg <- first){
result += arg
}
println(result)
}
fun5(10,20,30)
fun5(10,50)
  1. 递归函数。我们可以定义一个方法,使得方法自己调用自己,形成一个递归函数,但是方法的返回值类型必须显示的手动指定
1
2
3
4
5
6
7
8
9
10
def  fun6(first:Int):Int={
if(first <= 1){
1
}else{
first * fun6(first -1)
}
}

val fun6Result = hello6(10)
println(fun6Result)
  1. 定义一个方法,没有显示的指定返回值,那么我们方法当中定义的等号可以省掉

注意:如果省掉了=号,那么这个方法强调的就是一个代码执行的过程

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 定义了一个方法,但是方法的返回值没有显示指定,
* 此时我们就可以省掉方法定义的=号,如果省掉 = 号,
* 那么这个方法强调的是一个过程,代码执行的过程,
* 不会产生任何的返回值
* @param first
*/
def fun7(first:Int){
println(first)
30
}
fun7(20)
  1. 直接通过def定义一个方法
1
2
3
def fun8=10;
val fun8Result = fun8
println(fun8Result)
  1. 如果方法体当中只有一行代码,我们也可以省掉大括号
1
2
3
def fun9(first:Int,second:Int) = first+second
val fun9Result = fun9(10,20)
println(fun9Result)

定义函数

函数定义的两种形式

第一种形式:

1
val  函数名 = (参数名1:参数类型1,参数名2:参数类型2)  =>  {函数体}

第二种形式:

1
2
3
val  函数名 :(参数类型1,参数类型2) => (返回类型) = {
函数体
}

示例一:定义一个标准函数,使用 =>来进行定义

1
2
3
4
val func1 =(x:Int,y:Int) =>{
x+y
}
func1(2,8)

示例二:定义匿名函数。也就是我们可以定义一个没有名字的函数

定义一个匿名函数之后,这个函数就没法使用了

1
(x:Int,y:String) =>{x + y}

示例三:函数定义的另外一种形式,定义一个函数,参数只有一个且是Int类型,返回值也是Int类型

img

1
2
val func3 :Int => Int = {x => x * x }
val func3Result = func3(10)

示例四:定义一个函数,参数值是两个,分别是Int和String,返回值是一个元组,分别是String和Int

1
2
3
4
5
val func4:(Int,String) =>(String,Int) ={
(x,y) => (y,x)
}
val func4Result = func4(10,"hello")
println(func4Result)

函数与方法的区别以及转换

在scala当中,函数与方法是有区别的,函数可以作为一个参数,传入到方法里面去(高阶函数)

我们可以定义一个函数,然后再定义一个方法,但是方法的参数是一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val myFunc = (x:Int) =>{
x * x
}
val myFunc2 :(Int) => Int ={
x => x * x
}

def methodFunction(f:Int => Int):Int ={
println(f(100))
f(100)
}
val methodFunctionResult = methodFunction(myFunc)
val methodFunctionResult2 = methodFunction(myFunc2)
println(methodFunctionResult)
println(methodFunctionResult2)

方法可以自动转换成函数作为参数传递到方法里面去

1
2
3
4
5
6
def method2(x:Int) ={ x * x }
def methodFunc2(x:Int => Int):Int ={
x(100)
}
val methodFunc2Result = methodFunc2(method2)
println(methodFunc2Result)

我们可以通过 _ 将一个方法,转换成函数

1
2
3
4
5
6
def method3(x:Int,y:String ) :Int = {
println(x)
x
}
val methodToFunc = method3 _
println( methodToFunc)

数据结构

数组

元组

Map

列表

集合

队列

常用算子

闭包和柯里化

类和构造器

对象

继承

特质

模式匹配

范型与上界下界

视图界定与协变逆变

  • 本文作者: SANNAHA
  • 本文链接: https://sannaha.moe/Scala/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!