scala初识

Scala初识

The Absolute Scala Basics

Values, Variables and Types

学习一门新的语言,自然要先从数值类型入手。在scala中,常量用val定义,变量用var定义。

  • 定义常量Int

一个量有两种定义方式,一种是显式的定义x. 指明其为类型;另外一种则是隐式的声明,让编译器来判断的类型。推荐使用显式的定义,可读性更强一些。

1
2
val x : Int = 62
val x = 62
  • 定义常量String,同样分显式和隐式两种。
1
2
val aString : String = "hello"
val bString = "goodbye"
  • 定义布尔值Boolean
1
val aBoolean: Boolean = false
  • 定义字符类型 Char
1
2
val aChar: Char = 'a'
val bChar = 'b'
  • 定义Short类型整数
1
val aShort : Short = 3456
  • 定义Long 整数, 需要在数值尾部加上L
1
2
val aLong : Long =5273985273895237L
val bLong = 5273985273895237L
  • 定义浮点数Float, 需要在尾部加上f
1
2
val aFloat: Float = 2.0f
val bFloat = 2.0f
  • 定义双精度浮点数Double
1
2
val aDouble: Double = 3.14
val bDouble = 2.0

scala中还有一个特殊的类型:Unit类型,它表示无值,和其他语言中的void等同。用作不返回任何结果的方法的结果类型。Unit只有一个实例值,写成 ()

比如说:

1
2
val aWeirdValue = (aVariable = 3)// Unit == void
println(aWeirdValue)

Expressions

  • 操作符
1
2
3
val x = 3+5
val xIsEven = x%2 == 0
val xIsOdd = !xIsEven

scala中的操作符和Java中的操作符是一样的,包括||, && , >>,<<, ^ ,+= , 但是注意,没有++,—

  • if 表达式,if在scala中是以表达式的形式存在,而不是以指令的形式存在的,如下
1
2
val cond: Boolean = true
val i = if(cond) 42 else 0

当然也可以写正常的 If-else 指令

1
2
3
4
5
if(aVariable==4){
println("12dsdawd")
}else{
println(2112)
}
  • while表达式,在Scala中请不要写while表达式,因为scala有更方便的方法进行遍历。 while表达式也是可以返回值的,只不过返回一个unit。

在scala中,所有的代码都是以表达式的形式呈现的,因此我们在写scala的时候要尽量减少如 println()while、以及变量再赋值这类的命令式语言的代码。虽然他们有效,但本质上还是表达式,返回的值是Unit,且会干扰我们代码的工整

  • code block, 此外scala还有一种特殊的表达式 : CodeBlocks .这种表达式返回的值是代码块的最后一行得到的值,在这里 aCodeBlock = "goodbye". 此外要记住,在CodeBlocks中定义的val定量,在块外是无法被访问到的
1
2
3
4
5
val aCodeBlock = {
val y = 2
val z = y+1
if(z > 2) "hello" else "goodbye"
}

Exercise

1
2
3
4
5
6
7
val somValue = {
2<3
}
val someOtherValue = {
if(someValue) 239 else 2120
42
}

请问someOtherValue的结果是什么? 因为CodeBlock的值等于最后一行代码的值,在这里someOtherValue = 42

Functions

在Scala中,一个函数的定义如下:

aFunction是函数的名字,a和b是函数的两个需要被输入的参数,String是该函数的返回值类型

1
2
3
def aFunction (a: String,b: Int) String = {
a + " " + b
}

如果函数只有一行,那么可以不需要用{}

1
2
3
4
// 比如这个函数只需要返回42
def aParameterlessFunction(): Int = 42
// 这个函数负责打印,不需要返回值,因此返回一个Unit对象
def aFunctionWithSideEffects(aString: String): Unit = println(aString)

我们可以写一个递归函数:会连续打印n遍 aString。 在scala中,当我们需要写一个for/while循环的时候,我们需要写一个等价的递归函数。

1
2
3
4
5
6
def aRepeatedFunction(aString: String, n: Int): String = {
if (n == 1) aString
else aString + aRepeatedFunction(aString, n-1)
}

println(aRepeatedFunction("hello",3))

我们也可以在一个函数里面写另外一个函数

1
2
3
4
5
def aBigFunction(n: Int): Int = {
def aSmallerFunction(a: Int, b: Int): Int = a + b
aSmallerFunction(n, n-1)
}
println(aBigFunction(10))

Exercise

写一个函数,用来判断一个数是否为素数?

在其他语言中,我们可能会用一个for循环,每次循环都让n除以一个不大于其自身$\frac{1}{2}$ 的数(1除外),如果都不能整除,就说明这个数是素数。

在Scala中,由于我们要用递归去代替循环,那么就只能这么写:

1
2
3
4
5
6
7
def isPrime(n: Int): Boolean = {
def isPrimeUntil(t: Int): Boolean =
if (t <= 1) true
else n % t != 0 && isPrimeUntil(t-1)

isPrimeUntil(n / 2)
}

Type Inference

Scala有隐式的类型转换,比如说:

1
2
val x = 2
val y = x + "items"

此时,编译器认为一个Int+一个String还是String,因此y的类型就是一个String类型的常量。

但是,也存在编译器推测失灵的情况,主要发生在递归函数里面。我们看到,如果n<=0,会返回1,此时编译器会认为函数的返回类型为诶Int,但是第二行,返回的值类型是Int乘以一个函数调用,这就把便一起搞混了,到底是不是返回Int呢?

因此,在创建函数的时候最好点名函数的返回类型,如果返回类型为空,那么就将类型置为Unit

1
2
3
4
5
def factorial(n: Int): Int =
if (n <= 0) 1
else n * factorial(n-1)

println(factorial(5))

Recursion

使用递归函数的时候,很容易出现一种情况:栈溢出。就拿连乘函数来说,factorial(10) 还可以跑,但是factorial(5000) 就会报错。

那么怎么避免这个问题?

要解决问题就需要找到问题为什么会发生,if (n <= 0) 1 else n * factorial(n-1) 这句代码中,每调用一次factorial,就会占用一行栈空间。

如果n=5,那么这个递归的调用过程大致如下:

1
2
3
4
5
6
factorial(5)
5 * factorial(4)
5 * (4 * factorial(3))
5 * (4 * (3 * factorial(2)))
5 * (4 * (3 * (2 * factorial(1))))
5 * (4 * (3 * (2 * 1)))

为了解决这个问题,需要学习“尾递归”的概念。什么是尾递归?尾递归是指递归调用是函数的最后一个语句,而且其结果被直接返回,这是一类特殊的递归调用。 由于递归结果总是直接返回,尾递归比较方便转换为循环,因此编译器容易对它进行优化。

以上阶乘函数不是尾递归,因为递归调用的结果有一次额外的乘法计算,这导致每一次递归调用留在堆栈中的数据都必须保留。我们可以将它修改为尾递归的方式。

1
2
3
4
5
6
7
8
def anotherFactorial(n: Int): BigInt = {
@tailrec
def factHelper(x: Int, accumulator: BigInt): BigInt =
if (x <= 1) accumulator
else factHelper(x - 1, x * accumulator)
// TAIL RECURSION = use recursive call as the LAST expression
factHelper(n, 1)
}

以上的调用,由于调用结果都是直接返回,所以之前的递归调用留在堆栈中的数据可以丢弃,只需要保留最后一次的数据,这就是尾递归容易优化的原因所在, 而它的秘密武器就是上面的acc,它是一个累加器(accumulator,习惯上翻译为累加器,其实不一定非是“加”,任何形式的积聚都可以),用来积累之前调用的结果,这样之前调用的数据就可以被丢弃了。

因此,当我们需要用循环的时候,就可以用到 Tail Recursion

比如说我要用tail recursion来写一个判断是否为素数的函数,在这里我们的accumulator是一个布尔值的变量,用来记录在当前情况下,n是否还为素数。如果布尔值为否,那么就返回false,如果t<=1了,说除到2还是素数,说明n是真的素数,因此返回true。如果t还没到1,那么就递归调用IsPrimeUntil函数,将布尔值设定为 n%t != 0

1
2
3
4
5
6
7
8
9
10
def IsPrime(n: Int): Boolean ={

@tailrec
def IsPrimeUntil( isPrimeNow:Boolean, t:Int):Boolean ={
if(!isPrimeNow) false
else if(t<=1) true
else IsPrimeUntil(n%t!=0,t-1)
}
IsPrimeUntil( true,n/2)
}

比如我要写一个Fibonacci的递归函数,这个比较难一些。和一般的Fibonacci思维不同,如果使用tail recursion,是从底部想上去加,而且要有两个accumulator来记录$f(n-1),f(n-2)$的值

1
2
3
4
5
6
7
8
9
10
11
def fibonacci(n: Int): Int = {
@tailrec
def fiboTailrec(i: Int, last: Int, nextToLast: Int): Int =
if(i >= n) last
else fiboTailrec(i + 1, last + nextToLast, last)

if (n <= 2) 1
else fiboTailrec(2, 1, 1)
}

println(fibonacci(8)) // 1 1 2 3 5 8 13, 21

Call-by-Name and Call-by-Value

scala的call by name 和call by value最大的区别就是:

call-by-name在调用的时候会重新根据name做计算,而call-by-value预先计算,然后保留计算值后一直使用这个value。

比如说:

1
2
3
4
5
6
7
8
9
10
11
12
def calledByValue(x: Long): Unit = {
println("by value: " + x)
println("by value: " + x)
}
//注意,call by name的话,需要用到的语法不同,还要加上 =>
def calledByName(x: => Long): Unit = {
println("by name: " + x)
println("by name: " + x)
}

calledByValue(System.nanoTime())
calledByName(System.nanoTime())

结果如下图所示,我们看到,call by name的话,每次都会计算x的值;而call by value的话,会预计算x的值

同时call by name传入的表达式是lazy 的,也就是说,只有要用到这个参数的时候,才回去计算这个参数的值。

比如说:

1
2
3
4
5
def infinite(): Int = 1 + infinite()
def printFirst(x: Int, y: => Int) = println(x)

printFirst(infinite(), 34) // stack overflow
printFirst(34, infinite())

如上面这几行代码,我们定义了一个无限递归的函数infinite,如果调用infinite()必会导致stackoverflow

然后我们有定义了一个printFirst函数,第一个参数是call by value的,第二个是call by name的。

如果我们调用printFirst(infinite(), 34),会报错,因为x是call by value的,需要预计算,那么丢会调用infinite().导致栈溢出

但是如果我们调用printFirst(34,infinite()),就不会报错,因为y是call by name的,是lazy 的,若函数体内没有关于y的调用,就不会去计算y的值。因此不会报错。

当然,如果输入的都是数字的话,那么call by name和call by value就是一样的了

Default and Named Arguments

在写下尾递归的时候,我们常常会选择两个函数嵌套的写法,这是因为我们不想让用户手动输入accumulator的值,函数嵌套可以实现对用户的透明机制。

那么可不可以既实现透明,又只写一层函数的方法呢?显然,我们可以用默认参数

1
2
3
4
5
def trFact(n: Int, acc: Int = 1): Int =
if (n <= 1) acc
else trFact(n-1, n*acc)

val fact10 = trFact(10)

如果一个函数有多个默认参数的话,编译器会默认输入的第一个参数对应函数中第一个place holder,因此如果我们想让第一个参数默认,其他两个参数自己设置的话,就需要我们显式得设置参数的名字。

1
2
3
4
 def savePicture(format: String = "jpg", width: Int = 1920, height: Int = 1080): Unit = println("saving picture")
savePicture("jpg",800,600)// 这是可以的
savePicture(height = 600, width = 800)//这也是可以的
savePicture(800,600)//这就会报错

Smart Operations on Strings

现在我们来介绍一些对于字符串的操作

1
val str: String = "Hello, I am learning Scala"
  • 输出指定位置的字符
1
println(str.charAt(2)) // l
  • 节选字符串
1
2
println(str.substring(7,11))	//I am
println(str.take(2)) //He
  • 切分字符串
1
println(str.split(" ").toList) //List(Hello,, I, am, learning, Scala)
  • 判断字符串是否从某字符串开始
1
println(str.startsWith("Hello")) //True
  • 字符串替换
1
println(str.replace(" ", "-")) //Hello,-I-am-learning-Scala
  • 小写化
1
println(str.toLowerCase()) //hello, i am learning scala
  • 字符串长度
1
println(str.length) // 26
  • 字符串反转
1
println(str.reverse) // alacS gninrael ma I ,olleH
  • 字符串转为数字
1
2
val aNumberString = "2"
val aNumber = aNumberString.toInt
  • 字符串和数字拼接,需要用到特殊的拼接符号。

    • +: 用于在list的头部添加元素
    • :+ 用于在list尾部追加元素;
    1
    println('a' +: aNumberString :+ 'z')
  • S-插值器

S-插值器的语法有点类似于CSS、PHP中的变量。也就是说,利用S-插值器可以往字符串中插入变量,比如说”

1
2
3
4
5
val name = "David"
val age = 12
val greeting = s"Hello, my name is $name and I am $age years old"
val anotherGreeting = s"Hello, my name is $name and I will be turning ${age + 1} years old."
println(anotherGreeting) //Hello, my name is David and I will be turning 13 years old.
  • F-插值器

F-插值器的作用是用来格式化的

1
2
3
val speed = 1.2f
val myth = f"$name can eat $speed%2.2f burgers per minute"
println(myth) // David can eat 1.20 burgers per minute
  • raw-插值器

raw插值器则是让字符串中的转义符失效 。

1
2
3
4
5
6
7
val str2 = "a\nb"

val str3 = raw"a\nb"

println(str2) // \n 生效,a和b之间换行

println(str3) // a\nb , \n失效

但是我如果把string当做一个插入的参数打印,那么转义符就不会失效,比如:

1
2
3
4
5
6
7
val escaped = "This is a \n newline"
println(raw"$escaped")

/*
This is a
newline
*/

Object Oriented Programming in Scala

Object-Oriented Basics

现在我们来创建一个最简单的类

我们首先从构造类的参数说起。这个Person类需要两个构造参数:name和age,其中,age用val来修饰,说明age变成了类中的一个成员,我们可以用person.age来访问;但是name只是一个参数,我们无法用person.name 来获取这个参数。

然后我们来看类中的body部分,这一部分类似于CodeBlock,一些逻辑会直接运行。如果在里面定义了常量或者定量,他们都会变成fields我们都可以通过点运算符对其进行访问

接着我们来看两个函数,函数名都是greet,因此会发生函数重载。第一个greet函数接收一个String类型的参数,并打印字符串,这个字符串里面如果需要引用这个传参,需要用${this.name} ,而不能单纯的用$name

第二个函数没有任何输入参数,直接打印字符串,这里调用的参数是 $name ,是类里面的成员变量

和C++一样,scala也支持在类里面写多个构造函数,可以是没有参数的构造函数,也可以是有参数的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person(name: String, val age: Int = 0) {
// body
val x = 2
println(1 + 3)

// method
def greet(name: String): Unit = println(s"${this.name} says: Hi, $name")

// overloading
def greet(): Unit = println(s"Hi, I am $name")

// multiple constructors
def this(name: String) = this(name, 0) //如果只有一个string参数的构造函数
def this() = this("John Doe") //如果没有参数,其构造函数
}

val person = new Person("John", 26) //在创建对象时就会打印 4
println(person.x) // 2
person.greet("Daniel") //John says: Hi, Daniel
person.greet() //Hi, I am John

val person2 = new Person()
person2.greet() //Hi, I am John Doe

Exercise

  1. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /*
    Novel and a Writer

    Writer: first name, surname, year(出生年)
    - method fullname 返回全名

    Novel: name, year of release, author
    - authorAge 返回作者的年龄
    - isWrittenBy(author) 返回作者对象
    - copy (new year of release) = new instance of Novel 返回一个新实例
    */

如下:

1
2
3
4
5
6
7
8
9
class Writer(firstName: String, surname: String, val year: Int) {
def fullName: String = firstName + " " + surname
}

class Novel(name: String, year: Int, author: Writer) {
def authorAge = year - author.year
def isWrittenBy(author: Writer) = author == this.author
def copy(newYear: Int): Novel = new Novel(name, newYear, author)
}
  1. 1
    2
    3
    4
    5
    6
    7
    8
    9
    /*
    Counter class
    - receives an int value
    - method current count
    // 需要实现两个方法,一个增1,一个减1
    - method to increment/decrement => new Counter
    // 还要实现函数重载,输入一个n,要增加n次
    - overload inc/dec to receive an amount
    */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Counter(val count: Int = 0) {
def inc = {
println("incrementing")
new Counter(count + 1) // immutability
}
// def dec():Counter ={}
def dec = {
println("decrementing")
new Counter(count - 1)
}
// 这是一个递归函数,但是不是下尾递归
def inc(n: Int): Counter = {
if (n <= 0) this
else inc.inc(n-1)
}

def dec(n: Int): Counter =
if (n <= 0) this
else dec.dec(n-1)

def print = println(count)
}

由于函数返回一个Counter,所以可以在调用inc之后继续调用inc,如果调用了三次,那么会让count+3 = 3

但是如果重新调用inc,那么之前的inc并不会累加,对于counter来说,其count值始终为零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
调用: counter.inc.print
incrementing
1

调用: counter.inc.inc.inc.print
incrementing
incrementing
incrementing
3

调用: counter.inc(10).print
incrementing
incrementing
incrementing
incrementing
incrementing
incrementing
incrementing
incrementing
incrementing
incrementing
10

Syntactic Sugar: Method Notations

现在来介绍一些语法糖。scala是一个很”自然语言化”的语言,里面有很多特殊的语法,我觉得很像是C++中的运算符重载。

  • infix notation/ operator notation ,这种语法只适用于只有一个参数的函数中 。 在scala中,我们要理解一个概念:所有的操作符,同时也是一个函数。
1
2
3
4
5
6
class Person(val name: String, favoriteMovie: String,val age: Int = 0){
def likes(movie:String): Boolean = movie == favourite
def hangOutWith(person: Person): String = s"${this.name} is hanging out with ${person.name}"
}
val mary = new Person("Mary","Inception")
val tom - new Person("Tom","Fight Club")

我们要调用Person中的likes函数,正常的写法如下:mary.likes("Inception")

但我们可以直接这样写: mary likes "Inception", 也就是将其他特殊符号全部略去,只留下最重要的三个部分。又比如说,可以直接写 mary hangOutWith tom

甚至我可以重命名hangOutWith函数为 def +(person: Person): String = s"${this.name} is hanging out with ${person.name}", 那么就可以这么写: mary + tom

+.+ 是相等的,所以我们还可以这么写: mary.+(tom)

  • prefix notation,主要是 unary_ 前缀,它只适用于单目运算符-,+,~,!
1
2
val x = -1  // equivalent with 1.unary_-
val y = 1.unary_- // x和y是相等的

同样的,我们也可以在Person类中定义 名为unary_!的函数

1
2
3
4
class Person(val name: String, favoriteMovie: String,val age: Int = 0){
def unary_! : String = s"$name, what the heck?!"
def unary_+ : Person = new Person(name, favoriteMovie, age + 1)
}
1
2
3
4
5
6
7
//下面两个表达式是等价的 打印值: Hi, my name is Mary and I like Inception
println(!mary)
println(mary.unary_!)

// 下面两个表达式也是等价的,打印值: 1
println(+mary.age)
println(mary.unary_+.age)
  • Postfix notation 适用于没有任何参数的函数
1
2
3
4
5
class Person(val name: String, favoriteMovie: String, val age: Int = 0) {
def isAlive: Boolean = true //直接返回true
def apply(): String = s"Hi, my name is $name and I like $favoriteMovie"
def apply(n: Int): String = s"$name watched $favoriteMovie $n times"
}
1
2
3
4
5
import scala.language.postfixOps

println(mary.isAlive)
//用这种写法也可以,只不过需要导入一个包,不建议使用
println(mary isAlive)
  • apply () 函数

当我们直接在一个对象后面加上(),编译器就会自动调用该类中的apply函数

1
2
3
4
5
6
 class Person(val name: String, favoriteMovie: String, val age: Int = 0) { 
def apply(): String = s"Hi, my name is $name and I like $favoriteMovie"
def apply(n: Int): String = s"$name watched $favoriteMovie $n times"
}
println(mary.apply())
println(mary()) // equivalent

Method Notations (Exercises)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
1. Overload the + operator
mary + "the rockstar" => new person "Mary (the rockstar)"
*/
def +(nickname:String) = new Person(s"name($nickname)",favoriteMovie)
/*
2. Add an age to the Person class
Add a unary + operator => new person with the age + 1
+mary => mary with the age incrementer
*/
def unary_+ : Person = new Person(name,favoriteMovie,age+1)
/*
3. Add a "learns" method in the Person class => "Mary learns Scala"
Add a learnsScala method, calls learns method with "Scala".
Use it in postfix notation.
*/
def learns(lessons:String) = s"$name learns $lessons"
def learnScala = this learns "Scala"
/*
4. Overload the apply method
mary.apply(2) => "Mary watched Inception 2 times"
*/
def apply(n: Int): String = s"$name watched $favoriteMovie $n times"

Scala Objects

Scala既然可以当做面向对象的语言来写,那么就必须具备面向对象语言的特征。那么其中一个特征就是:类内静态方法、静态成员变量。静态方法我们在Java中学过,就是只能被类调用,而不能被实例调用。比如说:

1
2
3
class Person {
public static final int N_EYES = 2;
}

那么在scala中,如何实现类内静态成员呢? 答案就是Object

比如我创建一个名为Person的Object,里面的所有成员都是静态的,如果要调用,只能这样来写Person.N_EYES,Person.canFlay, Person.apply

此外,Object只有唯一一个实例,就是他自己,如果我令 mary = Person("Mary") ,又令 john = Person("John") 。那么事实上,mary和john是相等的,它们都是同一个实例。 专业一点叫做singleton instance

1
2
3
4
5
6
7
8
9
object Person { // type + its only instance
// "static"/"class" - level functionality
val N_EYES = 2
def canFly: Boolean = false
}

val mary = Person
val john = Person
println(mary == john) // true

那么如果要创建一个实例的的话,就需要另外创建一个class Person, 这里object 和 class后面的名字必须相同。可以这么说,Scala中的Object和Class共同构成了Java中的类

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
object Person { // type + its only instance
// "static"/"class" - level functionality
val N_EYES = 2
def canFly: Boolean = false
// 静态方法,只能通过Person.apply调用
// factory method
def apply(mother: Person, father: Person): Person = new Person("Bobbie")
}
class Person(val name: String) {
// instance-level functionality
}

//注意,如果既写了 object,又写了 class. 那么如果我们用new的话,一定是新建一个实例。
val mary = new Person("Mary") // 新实例 mary
val john = new Person("John") // 新实例 John
println(mary == john) // true

如果我想调用类内静态方法:

1
val bobbie = Person(mary,john)

最后,我们来说为什么我们现在都用object xxx extends App 来运行scala文件:

在Java中,要使程序能够运行,需要有一个main函数,如下所示:

1
2
3
4
5
public class Main { 
public static void main(String[] args) {
System.out.println("Hello World");
}
}

由于scala到最后也是需要放到JVM上去运行的,因此也需要有一个main函数。scala中的main函数如下:

1
def main(args: Array[String]): Unit

但是如果我们用 object xxx extends App ,那么就会继承 App中的main函数,我们就不用每次都写了。

Inheritance

既然上面提到了 继承,现在我们就来说说scala中的继承是怎么回事。

scala的继承以及一些关键字基本和Java中的类似。

单类继承

一个基本的单类继承如下:我创建了一个Animal类,然后有创建了一个Cat类去继承Animal,此时在Cat类中就可以继承Animal中的成员了。

成员默认是public,子类和外部都可以调用

如果成员被private修饰,那么其子类就无法调用和外部就无法调用

如果成员被protected修饰,那么其子类内部可以调用,但外部无法调用(实例调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sealed class Animal {
val creatureType = "wild"
def eat = println("nomnom")
}

class Cat extends Animal {
def crunch = {
eat
println("crunch crunch")
}
}

val cat = new Cat
cat.crunch

那么如果父类和子类的参数不同,该如何写继承语法?

比如说,Person有两个参数,Adult有三个参数,如果我们还是直接 extends Person ,编译器会报错。因此这样如果新建一个Adult实例的话,Adult事实上会调用Person中的构造函数,而Person中的构造函数只接受两个参数。因此,对于子类和父类参数不同的情况下,在继承时就要调用父类的构造函数,如下

1
2
3
4
5
6
7
8
9
class Person(name: String, age: Int) {
def this(name: String) = this(name, 0)
}
class Adult(name: String, age: Int, idCard: String) extends Person(name)//Person(name,age) 这里父类有两个构造函数,因此传入一个或者两个都可
/*
当然,如果父类中有这样的 def this() = this("112",0)构造函数
那么子类也可以直接 extends Person

*/

函数重写

在子类 可以重写父类中的函数和成员变量,需要使用override关键词。

当然,如果重写的是父类中的成员变量,可以放在子类的构造函数中,比如:Dog(override val creatureType: String)

1
2
3
4
5
class Dog(override val creatureType: String) extends Animal {
//override val creatureType = "domestic" // can override in the body or directly in the constructor arguments
override def eat = {
println("crunch, crunch")
}

super

如果想要在重写的函数中仍然运行父类中的代码,可以使用super,super就代表父类,比如:

1
2
3
4
5
class Dog(override val creatureType: String) extends Animal {
override def eat = {
super.eat
println("crunch, crunch")
}

事实上我们要尽量避免函数继承,因为会很乱。这里提供了一些办法来避免函数的重载:

  • 对成员变量、成员函数使用 final 关键词修饰,就可以避免被重写
  • 对类用final关键词修饰,就可以避免该类被继承
  • 新语法:使用seal关键词,如果用seal来修饰类,那么仅在此文件中可以继承该类,但是其他文件中无法继承该类

Inheritance, Continued: Abstract Classes and Traits

Abstract class

  • 抽象类的一个或者多个方法没有完整的定义
  • 声明抽象方法不需要加abstract关键字,只需要不写方法体,如def eat: Unit
  • 子类重写父类的抽象方法时不需要加override
  • 父类可以声明抽象字段(没有初始值的字段)
  • 子类重写父类的抽象字段时不需要加override
1
2
3
4
5
6
7
8
9
abstract class Animal {
val creatureType: String = "wild" // 非抽象字段
def eat: Unit //抽象方法
}

class Dog extends Animal {
override val creatureType: String = "Canine"
def eat: Unit = println("crunch crunch") //重写父类抽象方法时不需要加override
}

Traits

类似于Java中的接口,trait被用于通过所支持的方法特例化来定义对象。如Java 8中一样,Scala允许trait被部分实现。其有以下特征:

  • Abstract class和Traits 可以同时有抽象成员和非抽象成员
  • 但是和class相比,trait 没有构造函数.
  • 一个类只能继承一个抽象类,但可以混合多个traits。 比如说下面的Crocodile,继承了Animal,同时继承了两个traits: Carnivore和ColdBlooded
  • 一般 抽象类是描述一种物体的,而一个traits 是用来描述该物体的行为的
1
2
3
4
5
6
7
8
9
10
11
trait Carnivore {
def eat(animal: Animal): Unit
val preferredMeal: String = "fresh meat"
}

trait ColdBlooded
class Crocodile extends Animal with Carnivore with ColdBlooded {
override val creatureType: String = "croc"
def eat: Unit = println("nomnomnom") //重写抽象类中的抽象方法
def eat(animal: Animal): Unit = println(s"I'm a croc and I'm eating ${animal.creatureType}")//重写traits中的抽象方法
}

Type Hierarchy

最后提一嘴Scala中的类型结构,首先,所有类型都是继承自 scala.Any , 这就类似于JavaScript中的Object类, Any是整个Scala类型系统的超类

scala.Any下面分为两个类: scala.AnyValscala.AnyRef

  • scala.AnyVal类中,主要是Int,Unit,Boolean,Float这几个数值类
  • scala.AnyRef类中,主要是String,List, Set 这几个引用类型
  • scala.Noting类是一切类的子类,包括我们自己创建的类

Inheritance Exercises Implementing Our Own Collection

现在我们要实现一个Int类型的链表,链表要实现这样几个功能:

  • head:返回链表头部
  • def tail: 返回除了链表头部以外的剩余部分
  • def isEmpty: 返回链表是否为空
  • def add(int): 往链表中添加元素
  • def toString: 可以打印链表中的元素

根据上述要求我们写一个抽象类如下:我们要用函数式编程的思路去设计这个列表。所以添加一各元素,需要返回一个新的对象。

1
2
3
4
5
6
7
8
abstract class MyList {  
def head: Int
def tail: MyList
def isEmpty: Boolean
def add(element:Int): MyList
def printElements: String
override def toString : String = "["+printElements+"]"
}

然后我们要实现这个列表:由于一开始新建链表的时候,肯定需要一个空链表,因此我们要创建一个Empty的对象。这里Object没必要用class, 因为我们没有创建一个Empty实例然后去调用其内部方法的需求和必要。

然后创建 一个Cons类,同样继承MyList,这个类需要传入两个参数,链表的头部和其他部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
object Empty extends MyList{
def head: Int = throw new NoSuchElementException
def tail: MyList = throw new NoSuchElementException
def isEmpty: Boolean = true
def add(element : Int): MyList = new Cons(element,Empty)
def printElements: String = ""
}
class Cons(head:Int,tail:MyList) extends MyList{
def head: Int = head;
def tail: MyList = tail;
def isEmpty: Boolean = false;
def add(element:Int):MyList = new Cons(element,this)
//这是一个递归,每次只打印一个节点的头部
def printElements: String =
if (t.isEmpty)""+h
else h+" "+t.printElements
}

测试:

1
2
3
4
5
val list = new Cons(1,new Cons(2, new Cons(3,Empty))) // 1,2,3
println(list.tail.head) // 2
println(list.add(4).head) // 4
println(list.isEmpty) // false
println(list.toString) // [1 2 3]

Generics

现在来谈谈scala中的泛型类,这一块比较难

泛型类使用中括号 [] 接收类型参数,虽然类型参数可以是任何名字,但是一个惯例是使用字母 A 作为类型参数标识符。

1
2
3
4
5
6
//比如说我定义一个MyList的泛型类:
class MyList[A] {
// use the type A
}
//泛型类不一定只有一个参数,比如说我创建一个映射类,Key可以使一个数据类型,Value则可是另一种数据类型
class MyMap[Key,Value]

这个MyList泛型类使用A作为类型参数。这意味着这个 MyList[A] 只能存储类型为A的元素。因此,我可以创建一个字符串类型的列表,一个整数类型的列表:

1
2
val listOfIntegers = new MyList[Int]
val listOfStrings = new MyList[String]

然后我们来说说泛型方法:泛型方法就是接收了泛型参数的方法,如下

1
2
3
4
5
6
7
// generic methods
object MyList {
def empty[A]: MyList[A] = ??? //这是一个静态方法,返回MyList[A]
// ??? 是TODO的意思,这里只讲语法,因此不表
}
//比如我要创建一个空的存放整数类型的list,可以这样写:
val emptyListOfIntegers = MyList.empty[Int]

variance problem

现在我们来讨论一下泛型中多样化的问题

1
2
3
class Animal
class Cat extends Animal
class Dog extends Animal

我们来看,Cat是Animal的子类,Dog也是Animal的子类,那么,MyList[Cat]是不是MyList[Animal]的子类

我们有三种推测

  • MyList[Cat]MyList[Animal]的子类, 即 List[Cat] extends List[Animal],二者 Covariance(协变)
  • MyList[Cat]不是MyList[Animal]的子类,即 二者 Invariance(不可变)
  • MyList[Cat]反而要比MyList[Animal]更高一级,是MyList[Animal]的父类,即二者 Contravariance(逆变)

在scala中,这三种方法其实都可以实现,但是需要不同的符号:用加号表示为协变,减号表示逆变,如:

  • Convariance : 参数类型前面需要有一个+ 。如果是类型是协变的,那么我们可以用一个ConvarianceList[Cat] 去替换ConvariantList[Animal] ,说明Cat是Animal的子集

    那么这时候其实出了一个大问题:我们能不能加其他类型的动物进去?animalList.add(new Dog)合法吗?按照道理来说,Dog属于Animal,添加到animalList中是没有逻辑上的问题的,但是显然会污染一个cat类型的animallist。 如果我们要创建一个convariant的类,就必须解决这个问题——我们学完bounded types就知道了。

1
2
3
class ConvarianceList[+A]
val animal : Animal = new Cat
val animalList: ConvariantList[Animal] = new ConvarianceList[Cat]
  • Invariance :在scala中,什么符号都不加就是Invariance。在这种情况下,InvariantList[Animal] 只能对应 Animal类型
1
2
class Invariant[A]
val invariantAnimalList: InvariantList[Animal] = new InvariantList[Animal]
  • Contravariance: 参数类型前面需要有一个-, 这是最难以理解的,特别是如果我们还以列表为例的话——那么只能写成InvariantList[cat] = new InvariantList[Animal],我们用Animal可以去替换Cat,说明Animal是Cat的一个子类,语法上没问题但是逻辑上有问题。为了方便理解,我们可以重新设计一个类Trainer

    这样的话,一个动物训练师当然可以替换一个猫咪训练师,因为动物训练师可以训练所有动物,也包括猫。从这个角度上来看 逆变关系比较好理解。但是从动物训练师是猫咪训练师的一个子类来理解,就比较奇怪了。

1
2
class contravariantTrainer[-A]
val contravariantTrainer[cat] = new contravariantTrainer[Animal]

bounded types

在Scala中,类型界限是对类型参数或类型变量的限制。 通过使用类型边界,我们可以定义类型变量的限制。

  • 上界:这里T是类型参数,而S是类型。 通过将“上界”声明为“ [T <:S] ”,表示此类型参数T必须与S相同或S的子类型
1
2
3
4
// 这里,我们要求Cage中的输入参数必须是Animal的子类
class Cage[A <: Animal](animal: A)
// 因此,我们将Dog作为参数是合法的
val cage = new Cage(new Dog)

我们看到,如果是另外一个不是animal的子类,我们就没有办法将其放到Cage里面,因为Cage有类型限制

  • 下界:这里T是类型参数,而S是类型。 通过将“下界”声明为“ [T>:S] ”,表示此类型参数T必须与S相同或为S的超类型

现在我们可以解决上面那个可不可以把Dog插入 List[Cat]的难题了,解决方法就是,如果我往一个List[Cat]里面插入Dog,就会返回一个 List[Animal] ,也就是说把类型更泛化一个级别

1
2
3
4
5
class MyList[+A] { 
//要实现以上功能,我们可以用到下界符号,当与A同类型或者超类要加入到MyList[A]中,就会返回MyList[B]
//那么如果添加一个Dog,就会返回一个MyList[Animal]
def add(B >:A)(element:B): MyList[B] = ???
}

最后,我们对之前写的MyList进行修改:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
abstract class MyList[+A] { //将MyList改为泛型抽象类
def head: A //返回的参数从Int改为A
def tail: MyList[A] //同理,返回类型为A的MyList
def isEmpty: Boolean
def add[B >: A](element: B): MyList[B] //在add设置下界
def printElements: String
// polymorphic call
override def toString: String = "[" + printElements + "]"
}
}
//由于这里返回的是一个Exception,而Empty里面没有任何类型的数据,因此将Empty对象继承自MyList[Nothing]
//因为Nothing是任何类的子类,因此可以用Nothing来替换之前的Int
object Empty extends MyList[Nothing] {
def head: Nothing = throw new NoSuchElementException
def tail: MyList[Nothing] = throw new NoSuchElementException
def isEmpty: Boolean = true
//这里也要将函数改为下界,B >: Nothing,返回一个MyList[B]
def add[B >: Nothing](element: B): MyList[B] = new Cons(element, Empty)
def printElements: String = ""
}


class Cons[+A](head: A, tail: MyList[A]) extends MyList[A] /*注意这里只能是[A],不能是[+A]*/{
def head: A = h
def tail: MyList[A] = t
def isEmpty: Boolean = false
def add[B >: A](element: B): MyList[B] = new Cons(element, this)
def printElements: String =
if(t.isEmpty) "" + h
else s"$h ${t.printElements}"
}

// 测试如下:
object ListTest extends App {
val listOfIntegers: MyList[Int] = new Cons(1, new Cons(2, new Cons(3, Empty)))
val listOfStrings: MyList[String] = new Cons("Hello",new Cons("scala",Empty))
println(listOfStrings.toString) //[Hello scala]
println(listOfIntegers.toString)//[1 2 3]
}

Anonymous Classes

匿名类:也就是没有命名的类. 对于一个类的子类,如果我们只需要实现其一次(创建一个实例), 我们就可以使用匿名类

首先我们来看不使用匿名类的写法:

1
2
3
4
5
6
7
8
9
10
// 首先创建一个抽象类
abstract class Animal {
def eat: Unit
}
//然后创建抽象类的一个子类
class AnonymousClasses$$anon$1 extends Animal {
override def eat: Unit = println("ahahahahahaah")
}
// 最后为这个子类创建一个实例
val funnyAnimal: Animal = new AnonymousClasses$$anon$1

那么如果这个类的实例只会被创建一次,我们就没必要搞这么复杂,直接使用匿名类就可以:

1
2
3
4
5
6
7
abstract class Animal {
def eat: Unit
}

val funnyAnimal: Animal = new Animal {
def eat: Unit = println("ahahahahahaah")
}

匿名类不只有抽象类能使用,正常的类中也可以使用, 但是需要显式得写 override

1
2
3
4
5
6
7
class Person(name: String) {
def sayHi: Unit = println(s"Hi, my name is $name, how can I help?")
}

val jim = new Person("Jim") {
override def sayHi: Unit = println(s"Hi, my name is Jim, how can I be of service?")
}

Object-Oriented Exercises : Expanding Our Collection

现在我们在来扩展一下之前写的MyLIst,往里面添加三个函数: map,filter 和flatmap,示例和要求如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/*
Exercises:
首先创建两个接口
1. Generic trait MyPredicate[-T] with a little method test(T) => Boolean
2. Generic trait MyTransformer[-A, B] with a method transform(A) => B
然后创建三个函数
3. MyList:
- map(transformer) => MyList
- filter(predicate) => MyList
- flatMap(transformer from A to MyList[B]) => MyList[B]

class EvenPredicate extends MyPredicate[Int]
class StringToIntTransformer extends MyTransformer[String, Int]

[1,2,3].map(n * 2) = [2,4,6]
[1,2,3,4].filter(n % 2) = [2,4]
[1,2,3].flatMap(n => [n, n+1]) => [1,2,2,3,3,4]
*/
//一个Predicate接口,用来判断是否符合条件
trait MyPredicate[-T] {
def test(elem: T) : Boolean
}
//一个MyTransfomer结构,用来进行类型转换
trait MyTransformer[-A,B] {
def transform(elem : A): B
}

abstract class MyList[+A] {
/*
...
*/
// higher-order functions
def map[B](transformer: MyTransformer[A,B]): MyList[B]
def filter(predicate: MyPredicate[A]): MyList[A]
def flatMap[B](transformer: MyTransformer[A,MyList[B]]): MyList[B]
}

object Empty extends MyList[Noting] {
// 对一个Empty对象试行Map和filter操作,得到的肯定都是Empty本身
def map[B](transformer: MyTransformer[Nothing,B]):MyList[B] = Empty
def flatMap[B](tranformer:MyTransformer[Nothing,MyList[B]]):MyList[B] = Empty
def filter(predicate: MyPredicate[Nothing]): MyList[Nothing] = Empty
}

case class Cons[+A](h: A, t: MyList[A]) extends MyList[A] {
/* 对于List中的每一个元素,都进行predicate判定
+ 如果符合判定条件,就留下
+ 否则就递归判定tail

比如说 [1,2,3].filter(n%2 == 0)
= [2,3].filter(n%2 == 0)
= new Cons(2,[3].filter(n%2 == 0))
= new Cons(2,Empty.filter(n%2 == 0))
= new Cons(2,Empty)
*/
def filter(predicate: MyPredicate[A]) : MyList[A] =
if (predicate.test(h)) new Cons(h,t.filter(predicate))
else t.filter(predict)

/* 对于map来说,原先的列表有多长,新的列表就有多长,只不过对每个元素做了一个映射
比如说[1,2,3].map(n*2)
= new Cons(2,[2,3].map(n*2))
= new Cons(2,new Cons(4,[3].map(n*2)))
= new Cons(2,new Cons(4,new Cons(6,Empty)))
*/
def map[B](transformer: MyTransformer[A,B]):MyList[B] =
new Cons(transformer(h),t.map(transformer))

// flatmap比较难一些,因为列表中每个元素可能会被映射为多个元素
// 因此需要我们设计一个concatenation function

//这里用到了下界,只有相等类型或超类的列表才能连接起来,比如:
/* [1,2] ++ [3,4,5]
= new Cons(1,[2]++[3,4,5])
= new Cons(1,new Cons(2,Empty++[3,4,5]))
= new Cons(1,new Cons(2, new Cons(3,new Cons(4, new Cons(5)))))
*/
def ++[B >: A](list: MyList[B]): MyList[B] = new Cons(h, t ++ list)

/*然后我们就可以写flatmap函数了
比如说:[1,2].flatmap(n => [3n,4n])
= [3,4] ++ [2].flatMap(n => [3n,4n])
= [3,4] ++ [6,8] ++ Empty.flatmap(n => [n,n+1])
= [3,4,6,8]
*/
def flatMap[B](transformer: MyTransformer[A,MyList[B]]): MyList[B] =
transformer.transform(h)++t.flatMap(transformer)
}

// 测试
object ListTest extends App{
val listOfIntegers: MyList[Int] = new Cons(1, new Cons(2, new Cons(3, Empty)))
val anatherListOfIntegers: MyList[Int] = new Cons(4, new Cons(5, Empty))

//[2]
println(listOfIntegers.filter(new MyPredicate[Int]{
override def test(elem : Int): Boolean = elem % 2 == 0// elem % 2==0 是一个表达式
}))

//[2,4,6]
println(listOfIntegers.map(new MyTransformer[Int,Int]{
override def transform(elem: Int): Int = elem*2
}))

//[1,2,3,4,5]
println((listOfIntegers ++ anotherListOfIntegers).toString)

//[1,2,2,3,3,4]
println(listOfIntegers.flatMap(new MyTransformer[Int,MyList[Int]]{
override def transform(elem: Int): MyList[Int] =
new Cons(elem,new Cons(elem+1,Empty)).toString //返回一个新的MyList
}))
}

Case Classes

case class是scala中的一种更强大的创建类的方式。它不仅拥有普通class的功能,又有很多内建方法,不用我们自己去实现。

比如说我创建一个case class如下:

1
case class Person(name: String,age: Int)

现在来一一介绍case class的一些特性

  • 类的构造参数默认是类内成员
  • 初始化的时候可以不用new,也可以加上,但是普通类必须加new
1
2
val jim = Person("Jim",34)//初始化的时候可以不用new
println(jim.name)//这对于case class是合法的,因为name是类内成员;而对于普通class则无法编译
  • toString的实现更漂亮
1
2
3
4
5
6
println(jim)//Person(Jim,34) 我们看到直接打印case class的实例很直观的显示Person的参数

/*如果不加case的class,直接打印实例则是返回哈希字符串*/
class noCasePerson(name: String,age:Int)
val nocaseJim = new noCasePerson("Jim",34)//初始化必然要 new
println(nocaseJim)//lectures.part2oop.CaseClasses$noCasePerson@7a0ac6e3
  • 默认实现了equals 和hashCode;
1
2
3
4
5
6
7
/*在case class中,如果两个类参数相等,那么会默认判定这两个类是相等的(内置了equals方法)*/
val jim2 = Person("Jim",34)
println(jim = jim2)// true

/*但是在class中,则会返回false,因为这是两个实例*/
val nocaseJim2 = new noCasePerson("Jim",34)
println(nocaseJim = nocaseJim2) //false
  • case class 实现了 copy方法
1
2
val jim3 = jim.copy()//创建一个和jim一模一样的实例
val jim4 = jim.copy(age = 45)//创建一个除了age不同,其余都和jim一模一样的实例
  • Case class 在创建时同时创建了companion object(伴生对象),同时在里面给我们实现子apply方法,使得我们在使用的时候可以不直接显示地new对象;
1
val thePerson = Person//合法,这里的Person是case class Person的伴生对象
  • Case Class 可以序列化(Serializable)

  • Case Class 可以用于模式匹配,这是case class最重要的特性关于模式识别是什么,我们最后一章会学

  • 除了Case Class之外,还有Case Object,其主要特性和Case Class相同,但不会创建伴生对象——因为他们自己就是自己的伴生对象

Scala 3: Enums

现在我们来说说Scala中的枚举类,枚举类是Scala3中的新语法

基础定义

首先我们可以给出一个最基本的枚举类的定义,我们可以将scala中的enum理解为一中数据类型,case后面的是该类型可选的值

1
2
3
4
5
enum Permissions {
case READ,WRITE,EXECUTE,NONE
}

val somePermissions : Permissions = Permissions.READ

Enum中也可以有函数

在Enum中也可以定义函数:

1
2
3
4
5
6
7
8
9
10
enum Permissions {
case READ,WRITE,EXECUTE,NONE

def openDocument():Unit =
if(this == READ || this == WRITE) println("opening document")
else println("reading not allowed")
}

val somePermissions: Permissions = Permissions.READ
somePermissions.openDocument()// opening document

constructor args

Enum也可以接收参数:

1
2
3
4
5
6
enum Color(val rgb: Int){
case Red extends Color(0xFF0000)
case Green extends Color(0x00FF00)
case Blue extends Color(0x0000FF)
}
println(Color.Red.rgb) // 255 (即0xFF0000)

companion objects

我们可以为Enum创建伴生对象:

1
2
3
object Color{
def fromRGB(val rgb : Int) : Color = ??? //写从RGB到Color的逻辑(if-else),这里略去
}

Enum的一些标准接口

  • Enum.ordinal, 通过ordinal可以返回某个枚举值在类中的index,比如:
1
2
3
al somePermissions: Permissions = Permissions.READ 
// Read在Perssions中的index 是 0 ,因此 somePermissions.ordinal = 0
val somPermissionsOrdinal = somePermissions.ordinal
  • Enum.values 可以以数组的方式返回Enums中的所有值
1
2
3
4
println(Permissions.values.mkString("Array(", ", ", ")"))
//Array(READ, WRITE, EXECUTE, NONE)
println(Color.values.mkString("Array(", ", ", ")") )
//Array(Red, Green, Blue)
  • Enum.valueOf 这个方法感觉有点鸡肋..
1
2
println(Color.valueOf("Blue")) // Color.Blue 
println(Permissions.valueOf("READ")) // Permissions.READ

有了case class,我们可以让MyList的功能变得更加强大。

  • 首先有了equals方法,可以方便的比较两个List的元素是否相等
  • 能序列化,使得在分布式系统中更加方便操作

Exceptions

现在来学习scala中的异常处理。首先来分辨一下Error和Exception的区别:

  • Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError , StackOverFlowError, 都是 Error 的子类。

  • Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。Exception 又分为可检查(checked)异常和不检查(unchecked)异常【即运行时异常】,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。

    • 检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
    • 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略,类似 NullPointerException、ArrayIndexOutOfBoundsException 之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。

在JVM中,Exception和Error都继承了Throwable类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。

throw-catch

  • 首先来说怎么抛出一个异常:

要知道 throw new NullPointerException 也是一个表达式,返回一个Nothing, 所以我们可以用String类型接收它,因为Nothing是任何类的子类

1
val aWeirdValue: String = throw new NullPointerException // also crashes
  • 然后我们来说怎么catch一个异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def getInt(withExceptions: Boolean): Int =
if (withExceptions) throw new RuntimeException("No int for you!")
else 42

val potentialFail = try {
// 调用getInt函数,导致其抛出一个异常
getInt(true)
} catch {
// catch一个RuntimeException的异常的话,就打印
// 如果getInt抛出的是一个NullPointerException的话,虽然有try-catch,还是会报错
case e: RuntimeException => println("caught a runtime exception")
} finally {
// finally和java中一样,无论怎么样都会执行,可写可不写
// finally不会影响代码块的返回值类型
// use finally only for side effects
println("finally")
}

println(potentialFail)

自定义异常

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/*
1. Crash your program with an OutOfMemoryError
2. Crash with SOError
3. PocketCalculator
- add(x,y)
- subtract(x,y)
- multiply(x,y)
- divide(x,y)

Throw
- OverflowException if add(x,y) exceeds Int.MAX_VALUE
- UnderflowException if subtract(x,y) exceeds Int.MIN_VALUE
- MathCalculationException for division by 0
*/

// 首先我们要创建一些自定义异常类

class OverflowException extends RuntimeException("Overflow")
class UnderflowException extends RuntimeException("Underflow")
class MathCalculationException extends RuntimeException("Division by 0")


// 然后我们构造简易的计算器
case object PocketCalculator {
def add(x: Int,y:Int) = {
val result = x + y
if(x > 0 && y > 0 && x+y < 0) throw new OverFlowException
else if(x < 0 && y < 0 && x+y > 0 ) throw new UnderFlowException
else result
}

def substract(x: Int,y:Int) = {
val result = x - y
if(x > 0 && y<0 && x-y < 0) throw new OverFlowException
else if(x < 0 && y > 0 && x-y > 0 ) throw new UnderFlowException
else result
}

def multiply(x: Int,y:Int) = {
val result = x * y
if(x > 0 && y> 0 && x*y < 0) throw new OverFlowException
else if(x > 0 && y < 0 && x*y > 0) throw new UnderFlowException
else if(x < 0 && y < 0 && x*y < 0 ) throw new OverFlowException
else if(x < 0 && y > 0 && x*y > 0 ) throw new UnderFlowException
else result
}
def divide(x: Int, y: Int) = {
if (y == 0) throw new MathCalculationException
else x / y
}
}
//最后我们测试

val calculatorFail = try{
PocketCalculator.add(Int.MaxValue, 10)
}catch {
case e: OverflowException => println("OverflowException")
case e: UnderflowException => println("UnderflowException")
case e: MathCalculationException => println("MathCalculationException")
}
// OverflowException

Packaging and Imports

Functional Programming in Scala

What’s a Function, Really

我们要学习scala中的函数式编程特性,就先要弄明白函数式编程是什么。

函数式编程中的函数指的并不是编程语言中的函数(或方法),它指的是数学意义上的函数,即映射关系(如:y = f(x)),就是 y 和 x 的对应关系。

函数式编程的特性

  1. 函数是一等公民,它的意思就是函数与其他数据类型一样,可以把它们存在数组里,当做参数传递,赋值给变量,可以在任何地方定义,在函数内或函数外,可以作为函数的参数和返回值,也可以对函数进行组合。

  2. 高阶函数(Higher Order Function, HOF),在函数式编程中, 高阶函数的定义是把其它函数当做参数,或者返回一个函数作为结果的函数。

  3. 柯里化,就是把一个多参数的函数 f,转换为单参数函数 g,并且这个函数的返回值也是一个函数。

  4. Side Effects,所谓“副作用”,指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。
    在像 C++ 这样的命令式语言中,函数的意义与数学函数完全不同。例如,假设我们有一个 C++ 函数,它接受一个浮点参数并返回一个浮点结果。从表面上看它可能看起来有点像数学函数意义上的映射实数成实数,但是 C++ 函数可以做的不仅仅是返回一个取决于其参数的数字,它还可以读写其他的全局变量,也可将将输出写入屏幕并接收来自用户的输入。但是,在纯函数式语言中,函数只能读取其参数提供给它的内容,并且它对世界产生影响的唯一方式就是通过它返回的值。

  5. 纯函数,纯函数编程和函数编程的区别在于:是否允许在函数内部执行一些非函数式的操作,同时这些操作是否会暴露给系统中的其他地方?也就是是否存在副作用。如果不存在副作用,或者说可以不用在意这些副作用,那么就将其称为纯粹的函数式编程。

  6. 引用透明性,函数无论在何处、何时调用,如果使用相同的输入总能持续地得到相同的结果,就具备了函数式的特征。这种不依赖外部变量或“状态”,只依赖输入的参数的特性就被称为引用透明性(referential transparency)。“没有可感知的副作用”(比如不改变对调用者可见的变量,进行I/O,不抛出异常等)的这些限制都隐含着引用透明性

  7. 递归和迭代,对于函数式而言,循环体有一个无法避免的副作用,就是它会修改某些对象的状态,通常这些对象又是和其他部分共享的。而且也因为变量值是不可变的,纯函数编程语言也无法实现循环。所以纯函数编程语言通常不包含像 while 和 for 这样的迭代构造器,而是采用的无需修改的递归。

function value

Scala编译后是要放到JVM上运行的,其实本质上是一个面向对象的编程语言。但是为了让他可以实现函数式编程,就需要对其方法(method)做一定的包装(变为函数)。因此,我们要理解——在scala里面,函数和方法是两个不同的概念。

  • 函数接口是 FunctionN(N可以是1,2,3…代表接收的参数个数) ,将其理解为一个实体,在实体中包装了一个行为方式,写在了其apply方法里
  • 方法是 Method(在实体中用def定义),将其理解为一种行为方式,这个行为方式可以通过转换来包装成一个FunctionN实体

比如说,我想创建一个函数,它可以将字符串转移成数字,那么显然它接受1个参数,并返回一个Int类型的值。那么此时我需要用到Function1接口:

1
2
3
4
5
6
// stringToIntConverter 接收String类型的参数,返回 Int类型的值
val stringToIntConverter = new Function1[String, Int] {
override def apply(string: String): Int = string.toInt
}

println(stringToIntConverter("3") + 4) // 7

Function1接口的源码如下:我们看到,其内部只有一个apply函数,之前我们学过,在一个对象后面加上(),编译器就会自动调用该类中的apply函数

1
2
3
4
5
6
@annotation.implicitNotFound(msg = "No implicit view available from ${T1} => ${R}.")
trait Function1[@specialized(Specializable.Arg) -T1, @specialized(Specializable.Return) +R] extends AnyRef { self =>
/** Apply the body of this function to the argument.
* @return the result of function application.
*/
def apply(v1: T1): R

再比如说,我想创建一个函数,它可以实现两个Int值相加,那么此时我需要用Function2接口

1
2
3
 val adder = new Function2[Int, Int, Int] {
override def apply(a: Int, b: Int): Int = a + b
}

语法糖

事实上,我们在创建函数的时候, Function2[A,B,R](A,B) => R是等价的,因此为了简介,我们也可以直接这样写:

1
2
3
val adder: ((Int, Int) => Int) = new Function2[Int, Int, Int] {
override def apply(a: Int, b: Int): Int = a + b
}

是不是感觉比原来更复杂了,我们接下来学习匿名函数的时候会对其做一个简化

练习

  • write a function which takes 2 strings and concatenates them
1
2
3
val concatenator:((String,String) => String ) = new Function2[Int,Int,Int]{
override def apply (a:String, b:String): String = a+b
}
  • transform the MyPredicate and MyTransformer into function types

要对MyList中的MyPredicate和MyTransformer做一个改造,使其成为function types. 事实上,我们可以直接删去这两个接口

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package exercises

abstract class MyList[+A] {
//主要改这三个函数
/*
我们要把圈来的 Mytransformer接口和MyPredicate接口改成函数式接口,可以是 Function1[A,B]
但是为了更加直观,我们可以将其直接写为 A => B
higher-order functions
*/
def map[B](transformer: Function1[A,B]): MyList[B]
def flatMap[B](transformer: A => MyList[B]): MyList[B]
def filter(predicate: A => Boolean): MyList[A]
}
}

case object Empty extends MyList[Nothing] {
def map[B](transformer: Nothing => B): MyList[B] = Empty
def flatMap[B](transformer: Nothing => MyList[B]): MyList[B] = Empty
def filter(predicate: Nothing => Boolean): MyList[Nothing] = Empty
}

case class Cons[+A](h: A, t: MyList[A]) extends MyList[A] {

def filter(predicate: A => Boolean): MyList[A] =
if (predicate(h)) new Cons(h, t.filter(predicate))
else t.filter(predicate)

def map[B](transformer: A => B): MyList[B] =
new Cons(transformer(h), t.map(transformer))

def ++[B >: A](list: MyList[B]): MyList[B] = new Cons(h, t ++ list)

def flatMap[B](transformer: A => MyList[B]): MyList[B] =
transformer(h) ++ t.flatMap(transformer)
}

object ListTest extends App {
val listOfIntegers: MyList[Int] = new Cons(1, new Cons(2, new Cons(3, Empty)))
val cloneListOfIntegers: MyList[Int] = new Cons(1, new Cons(2, new Cons(3, Empty)))
val anotherListOfIntegers: MyList[Int] = new Cons(4, new Cons(5, Empty))

println(listOfIntegers.filter(new Function1[Int,Boolean]{
override def apply(elem : Int): Boolean = elem % 2 == 0
}))

println(listOfIntegers.map(new Function1[Int,Int]{
override def apply(elem: Int): Int = elem*2
}))

println((listOfIntegers ++ anotherListOfIntegers).toString)

println(listOfIntegers.flatMap(new Function1[Int,MyList[Int]]{
override def apply(elem: Int): MyList[Int] =
new Cons(elem,new Cons(elem+1,Empty)).toString //返回一个新的MyList
}))
}
  • define a function which takes an int and returns another function which takes an int and returns an int

对于这个问题我们要从两个方面来思考:

  1. 这个函数是什么类型的?接收一个Int,返回一个function,因此是Function1类型的
  2. 怎么实现?如下
1
2
3
4
5
6
7
8
9
10
11
val superAdder: Function1[Int, Function1[Int, Int]] = new Function1[Int, Function1[Int, Int]] {
// 在这里实现apply函数,接收一个Int,返回一个Function type
override def apply(x: Int): Function1[Int, Int] = new Function1[Int, Int] {
override def apply(y: Int): Int = x + y
}
}

val adder3 = superAdder(3)// 此时adder3是外层的apply函数,仍然接收一个Int
println(adder3(4)) // adder3(4)其实调用的是内部的apply函数,返回值是int,结果是7
println(superAdder(3)(4)) // curried function 7
//superAdder就是一个Curried function,他将原来多参数的函数变为单参数的函数,

Anonymous Functions

不知道你们有没有发现,我们利用Function type来实现函数接口,从语法上还是遵循了 定义类、重载函数的思想,实际上还没有摆脱面向对象的编程思维。

因此我们可以使用 匿名函数,也就是 JavaScript中的 Lambda函数。 Scala 中定义匿名函数的语法很简单,箭头左边是参数列表,右边是函数体。

使用匿名函数后,我们的代码变得更简洁了。比如:

1
val doubler = (x:Int) => x*2

同样我们可以在匿名函数中定义多个参数:

1
val adder = (x: Int, y: Int) => x+y

也可以不给匿名函数传入参数:

1
val justDoSth = () => 3

需要注意,println(justDoSth)println(justDoSth()) 是不同的,前者是打印函数本身,后者是打印调用函数后返回的结果

语法糖

我们再来介绍一个匿名类中的语法糖:可以用下划线来代替传入的参数

1
2
val niceIncrementer: Int => Int = _ + 1 // equivalant to x => x+1
val niceAdder: (Int,Int) => Int = _+_ // equivalent to (a,b) => a+b

Exercise

现在我们将MyList中的FunctionN接口都改为匿名函数

改之前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
object ListTest extends App {
val listOfIntegers: MyList[Int] = new Cons(1, new Cons(2, new Cons(3, Empty)))

println(listOfIntegers.filter(new Function1[Int,Boolean]{
override def apply(elem : Int): Boolean = elem % 2 == 0
}))
println(listOfIntegers.map(new Function1[Int,Int]{
override def apply(elem: Int): Int = elem*2
}))
println(listOfIntegers.flatMap(new Function1[Int,MyList[Int]]{
override def apply(elem: Int): MyList[Int] =
new Cons(elem,new Cons(elem+1,Empty)).toString //返回一个新的MyList
}))
}

改之后:

1
2
3
4
5
6
7
8
9

object ListTest extends App {
val listOfIntegers: MyList[Int] = new Cons(1, new Cons(2, new Cons(3, Empty)))

println(listOfIntegers.filter(_%2 == 0).toString)
println(listOfIntegers.map(_ * 2))
println(listOfIntegers.flatmap(elem => Cons(elem, Cons(elem+1,Empty)).toString)
}))
}

然后我们将之前的 super adder改成匿名函数

1
2
3
4
5
6
7
8
//改之前
val superAdder: Function1[Int, Function1[Int, Int]] = new Function1[Int, Function1[Int, Int]] {
override def apply(x: Int): Function1[Int, Int] = new Function1[Int, Int] {
override def apply(y: Int): Int = x + y
}
}
//改之后,接收了x参数,返回一个需要接受y的函数,然后再返回x+y
val superAdd = (x: Int) => (y: Int) => x + y

Higher-Order-Functions and Curries

我们之前说过scala中的高阶函数要么接收一个函数为参数,要么返回一个函数。

比如说,我想设计一个可以重复将某一函数执行n次的函数,那么就需要传入一个函数f、参数n、初始值x

nTimes(f,3,x) = nTimes(f,2,f(x)) = nTimes(f,1,f(f(x)))=nTimes(f,0,f(f(f(x))))

1
2
3
4
5
6
7
def nTimes(f:Int => Int, n: Int, x:Int) : Int = 
if(n <= 0) x
else nTimes(f,n-1,f(x))

//调用

print(nTimes(plusOne,100000,1))

但我们有没有感到一丝奇怪,就是我们说的函数式编程,是希望像数学里的那样,实现一个映射关系。每次只对一个参数x进行操作,而不是像上面的调用那样,一次要输入3个参数

因此我们可以对nTimes函数做一个改进:改进后的nTimes,每次返回的不再是Int,而是一个(Int => Int)的函数, 因此,我们可以调用返回的函数去计算

但是这种方法也有弊端,就是会出现栈溢出的问题,不是下尾递归

1
2
3
4
5
6
7
def nTimesBetter(f: Int => Int, n: Int): (Int => Int) = 
if (n <= 0)(x: Int) => x
else (x: Int) => nTimesBetter(f,n-1)(f(x))

//调用,此时,plus10变成了一个 Int=>Int的函数
val plus10 = nTimesBetter(plusOne,100)
println(plus10(1)) // 11

多个参数列表

多个参数列表(multiple parameter lists)常常和柯里化常常一起用,比如说,我想创建一个函数,它可以将double类型的浮点数格式化成想要的样子。正常来说,需要输入两个参数,一个是需要被格式化的浮点数,另一个是String类型的格式本身。但是若要对其进行柯里化,就只能让函数每次只接收一个参数。

因此,我们可以使用多个参数列表的语法。也就是使用多个(), 注意,括号的顺序和参数输入的顺序是有关的。比如说下面这个curriedFormatter,需要先输入String, 然后在去处理Double,最后返回String

其本质上是一个String => Double => String 的函数

1
2
3
4
5
6
7
8
9
def curriedFormatter(c: String)(x: Double) : String = c.format(x)

//测试:
//curriedFormatter返回一个Double=>String的函数
val standardFormat: (Double => String) = curriedFormatter("%4.2f")
val preciseFormat: (Double => String) = curriedFormatter("%10.8f")

println(standardFormat(Math.PI)) // 3.14
println(preciseFormat(Math.PI)) // 3.1415926

需要注意的是,如果使用 多个参数列表的话,我们在定义子函数的时候(如上面的standardFormat和preciseFormat),需要显式得注明函数接受的参数及其返回类型。

为了把多个参数列表讲清楚,我们在来举一个三个参数列表的例子:

  • 首先,我定义了triplefunc,它是一个String => Double => Int =>String的函数
  • 然后,我有定义了一个doubleFormatter, 它是triplefunc接收了一个参数后的返回值,类型为Double => Int => String
  • 接着,我又定义了一个IntAdder,它是doubleFormatter接收了一个参数后的返回值,类型为Int=>String
1
2
3
4
5
6
7
def triplefunc(c:String)(x:Double)(y:Int):String = c.format(x+y)
val doubleFormatter: Double => Int => String = triplefunc("%4.2f")
val IntAdder: Int => String = doubleFormatter(2.33)
//最后我们将三个函数全部调用,得到的结果是一样的 6.33
println(triplefunc("%4.2f")(2.33)(4))
println(doubleFormatter(2.33)(4))
println(IntAdder(4))

Exercises

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
1. Expand MyList
- foreach method A => Unit
[1,2,3].foreach(x => println(x))

- sort function ((A, A) => Int) => MyList
[1,2,3].sort((x, y) => y - x) => [3,2,1]

- zipWith (list, (A, B) => C) => MyList[C]
[1,2,3].zipWith([4,5,6], x * y) => [1 * 4, 2 * 5, 3 * 6] = [4,10,18]

- fold(start)(function) => a value
[1,2,3].fold(0)(x + y) = 6

2. toCurry(f: (Int, Int) => Int) => (Int => Int => Int)
fromCurry(f: (Int => Int => Int)) => (Int, Int) => Int

3. compose(f,g) => x => f(g(x))
andThen(f,g) => x => g(f(x))
*/

Exercise1

首先对MyList进行改进,新加入了四个高阶函数

  • abstract class
1
2
3
4
5
6
abstract class MyList[+A]{
def foreach(f: A => Unit) : Unit
def sort(compare:(A,A)=>Int) => MyList
def zipWith[B,C](list:MyList[B],zip:(A,B)=>C):MyList[C]
def fold[B](start:B)(operator:(B,A)=>B):B
}
  • object Empty
1
2
3
4
5
6
7
8
case object Empty extends MyList[Nothing] {
def foreach(f: Nothing => Unit):Unit = ()
def sort(compare:(Nothing,Nothing)=>Int):MyList = Empty
def zipWith[B,C](list:MyList[B],zip:(Nothing,B)=>C):MyList[C]=
if(!list.isEmpty) throw new RuntimeException("Lists do not have the same length")
else Empty
def fold[B](start:B)(operator:(B:Nothing)=> B):B = start
}
  • class Cons
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
case class Cons[+A](h: A, t: MyList[A]) extends MyList[A] {
def foreach(f: A => Unit):Unit = {
f(h)
t.foreach(f)
}
def sort(compare:(A,A)=>Int):MyList = {
def insert(x: A, sortedList: MyList[A]): MyList[A] =
if (sortedList.isEmpty) new Cons(x, Empty)
else if (compare(x, sortedList.head) <= 0) new Cons(x, sortedList)
else new Cons(sortedList.head, insert(x, sortedList.tail))

val sortedTail = t.sort(compare)
insert(h, sortedTail)
}

def zipWith[B,C](list:MyList[B],zip:(A,B)=>C):MyList[C] =
if (list.isEmpty) throw new RuntimeException("Lists do not have the same length")
else new Cons(zip(h,list.head),t.zipWith(list.tail,zip))

def fold[B](start:B)(operator:(B,A)=>B):B =
t.fold(operator(start,h))(operator)
}

map, flatMap, filter and for-comprehensions

我们之前实现了自己的List,并在其中实现了map,flatmap和filter的相关功能,现在来介绍Scala中的内置方法。

首先来创建一个List:

1
2
3
val list = List(1,2,3)
println(list.head)// 打印头部,即1
println(list.tail)// 打印除了头之外的部分,List(2, 3)
  • map
1
2
println(list.map(_ + 1))							//List(2, 3, 4)
println(list.map(_ + " is a number")) //List(1 is a number, 2 is a number, 3 is a number)
  • filter
1
println(list.filter(_ % 2 == 0)) 	//List(2)
  • flatMap
1
2
val toPair = (x: Int) => List(x, x+1)
println(list.flatMap(toPair)) //List(1, 2, 2, 3, 3, 4)
  • foreach
1
list.foreach(println)// 1,2,3

我们看到这些函数和我们之前自己实现的功能是一样的。

多重循环

现在如果我想输出两个甚至更多个list的笛卡尔积,该如何操作?在面向对象的语言中,我们会使用双重循环for-loops,但是在scala中,我们需要用flatmap和map的组合来实现

  • 如果是双重循环,外循环中的每个值,都会产生一个list,是一对多的映射,因此使用flatMap;内循环每个值只生成一个对应的值,是一对一映射,因此使用map
  • 如果是三重循环,除了最内层循环使用map,外层循环都会产生一个list,因此使用flatMap
1
2
3
4
5
6
7
8
9
10
11
  val numbers = List(1,2,3,4)
val chars = List('a','b','c','d')
val colors = List("black", "white")

//如果我想做a1,a2,...d3,d4
println(numbers.flatMap(n => chars.map(c => ""+c+n)))
//List(a1, b1, c1, d1, a2, b2, c2, d2, a3, b3, c3, d3, a4, b4, c4, d4)

//如果我想对三个list做笛卡尔积:
println(numbers.flatMap(n => chars.flatmap(c => colors.map(color=>""+c+n+"-"+color))))
//List(a2-black, a2-white, b2-black, b2-white, c2-black, c2-white, d2-black, d2-white, a4-black, a4-white, b4-black, b4-white, c4-black, c4-white, d4-black, d4-white)=

事实上,这种代码的可读性是比较差的,因此,scala提供了多重循环的简化版本。也就是 for循环,如下:

1
2
3
4
5
6
7
val forCombinations = for {
// guards, 也就是for循环中的if条件判断,编译器会将其翻译为filter
n <- numbers if n % 2 == 0
c <- chars
color <- colors
} yield "" + c + n + "-" + color
println(forCombinations)

for 循环中的 yield 会把当前的元素记下来,保存在集合中,循环结束后将返回该集合。Scala中 for 循环是有返回值的。如果被循环的是 Map,返回的就是 Map,被循环的是 List,返回的就是 List,以此类推。

上面这句话的意思就是,对于numbers中的偶数、chars中的字符,colors中的颜色,都将其组合并保存在集合中

但是这只是一种语法糖,在编译器内部,还是将for语句转换成map和flatmap来执行的。

  • 注意,以下两种写法都是可以的,属于语法重载。
1
2
3
4
5
list.map { x =>
x * 2
}

list.map(_*2)

发散

试问,我们之前创建的MyList对象,是不是也可以使用for语句进行循环呢?可以的,只要我们在函数内定义了逻辑正确的map、flatMap、filter函数,就可以应用for

1
2
3
4
5
6
7
8
  val listOfIntegers: MyList[Int] = new Cons(1, new Cons(2, new Cons(3, Empty))) 
val listOfStrings: MyList[String] = new Cons("Hello", new Cons("Scala", Empty))

val combinations = for {
n <- listOfIntegers
string <- listOfStrings
} yield n + "-" + string
println(combinations)//[1-Hello 1-Scala 2-Hello 2-Scala 3-Hello 3-Scala]

A Collections Overview

这一节我们来学习scala中的 集合类,这是一个比较大的类,包含了很多子类.

首先,我们要了解scala中的集合分为 mutable(可变) collection 和immutable(不可变) collection。

  • 可变集合可以在适当的地方被更新或扩展。这意味着你可以修改,添加,移除一个集合的元素。
  • 不可变集合类,相比之下,永远不会改变。不过,你仍然可以模拟添加,移除或更新操作。但是这些操作将在每一种情况下都返回一个新的集合,同时使原来的集合不发生改变。

之前我们自己写的MyList,List都属于immutable collection之列。

mutable collection架构图

在本篇文章中不会介绍mutable collection的相关内容

immutable collection 架构图

Traversable是所有collection的母类,然后在Iterable下面主要分三大类:Set,Map, 和Seq。 Set是不包含重复数据的集合, Maps是键值对集合,Seq是适合存有序重复数据的列表集合。

Seq下面还是有 IndexSeq 和 LinearSeq两种, 前者可以通过 索引来快速定位元素。后者的元素则是线性的,按照某种顺序排列的。

Sequences: List, Array, Vector

首先我们来看看 Seq接口提供了哪些操作:

1
2
3
4
trait Seq[+A] {
def head: A
def tail : Seq[A]
}

Seq接口是一个很general(不知道怎么翻译了)的接口,其中的元素是按照清晰地顺序进行排列的。而且可以通过下标index进行访问。

Seq接口提供很多操作:

  • 在索引和迭代方面

    • apply: 可以直接通过Seq(1,3,2,4) 这样的方法构造Seq
    • iterator: 详细用法可以参考文档
    • length:返回seq长度
    • reverse: 倒转seq
  • 创建新的Seq方面

    • concatenation: 两个seq连接
    • appending: seq之后连一个元素
    • prepending: seq之前连一个元素
  • 其他
    • grouping
    • sorting
    • zipping
    • searching
    • slicing
1
2
3
4
5
6
val aSequence = Seq(1,3,2,4)
println(aSequence) // List(1, 3, 2, 4)
println(aSequence.reverse)// List(4, 2, 3, 1)
println(aSequence(2)) // 2
println(aSequence ++ Seq(7,5,6))// List(1, 3, 2, 4, 7, 5, 6)
println(aSequence.sorted) // List(1, 2, 3, 4)

Ranges

scala中的Range也是一种类型,其本质上是一种特殊的 Array

range有两种定义方式:

  • 利用until / to 定义 . to代表前闭后闭区间;until代表前闭后开区间
1
2
3
4
5
val aRange: Seq[Int] = 1 until 10 
aRange.foreach(println)//1 2 3 4 5 6 7 8 9

val bRange: Seq[Int] = 1 to 10
bRange.foreach(println)//1 2 3 4 5 6 7 8 9 10
  • 利用Range(A,B) by C 定义。Range(A,B)代表A和B的前闭后开区间 ,C代表自定义步长
1
2
3
//范围需要在最大值和最小值范围内即 2 <= 元素 <= 19
val res: Range = Range(2, 20) by 3
println(res.foreach(println))

利用 Ranges我们可以实现类似循环的效果:

1
(1 to 10).foreach(x => println("Hello")) // 打印10遍 Hello

List

List列表:不可变

LIst支持在头部快速添加和移除条目,意味着模式匹配很顺畅, head、tail、isEmpty操作只需要 $O(1)$的复杂度。

大多数List操作还是需要遍历整个列表的,需要O(n)的复杂度,如length,reverse。

List不支持索引直接定位,如 list(2)=10 (非法); 但是可以使用updated方法,但此方法也不是索引定位,而是线性复杂度的。

List列表跟其他语言中的数组非常像,二者都是同构的,同一个列表的所有元素必须是相同类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val aList = List(1,2,3)
// +: 是prepend, :+ 是 append,我们只要记住 : 都是靠近List一边的即可
val prepended = 42 +: aList :+ 89
println(prepended) // List(42, 1, 2, 3, 89)

/*
fill函数是创建一个长度为5的数组,数组中每个元素都是 字符串 apple
*/
val apples5 = List.fill(5)("apple")
println(apples5)
/*
将list中的元素按一定格式排列,mkString中的参数就是每个元素之间的分割符
*/
println(aList.mkString("-|-"))

Array

Array和List不一样,它和Java中的arrays是等价的

  • 在创建的时候,可以预先其分配内存,而不对其进行赋值
  • 在适当的位置可以被修改(更新),所以是部分可变的
  • 可以和 Java的 T[] arrays互相操作
  • 可以通过索引访问,速度很快
1
2
3
4
5
6
7
8
9
10
// 这是创建一个长度为4的Array,同时赋值
val numbers = Array(1,2,3,4)
//创建一个String类型、长度为3的Array,但不赋值
val threeElements = Array.ofDim[String](3)
threeElements.foreach(println)// 打印会得到null
//如果是数值类型的数组,不赋值,打印会得到0或者false(bool)

// array是部分可变的,比如我们可以将某一位置上的值更新。
numbers(2) = 0 // 这种写法比较直白,其实是numbers.update(2, 0)的语法糖
println(numbers.mkString(" "))
  • seq和array之间的转化
1
2
3
// arrays and seq
val numbersSeq: Seq[Int] = numbers // 隐式转化,将array转换为ArraySeq
println(numbersSeq) //ArraySeq(1 2 0 4)

Vector

vector是另外一个独立的体系,也是 Imuutable的

  • vector可以利用索引来进行读写,复杂度为$O(\log_{32}(n))$,因此vector的索引操作是非常快的
  • 可以使用append和prepend
  • 底层数据结构是fixed-branched Trie树(字典树),
  • 当vector中数据很大时,表现很好
  • 可以使用prepend和append方法,也可以调用 updated 进行更新
1
2
val vector: Vector[Int] = Vector(1,2,3)
println(vector)
  • vectors和lists性能对比

在数据量很大的情况下,vectors的表现要远远优于list:

首先我们写一个函数,对一个长度为1,000,000的Seq(vector和list都继承自seq),随机替换seq中的某一个值,重复1,000遍。最终返回平均每次updated操作的运算时间。

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
val maxRuns = 1000
val maxCapacity = 1000000

def getWriteTime(collection: Seq[Int]): Double = {
val r = new Random
val times = for {
it <- 1 to maxRuns
} yield {
val currentTime = System.nanoTime()
collection.updated(r.nextInt(maxCapacity), r.nextInt())
System.nanoTime() - currentTime
}

times.sum * 1.0 / maxRuns
}

val numbersList = (1 to maxCapacity).toList
val numbersVector = (1 to maxCapacity).toVector

// keeps reference to tail
// updating an element in the middle takes long
println(getWriteTime(numbersList)) //耗费 2673515.435 纳秒
// depth of the tree is small
// needs to replace an entire 32-element chunk
println(getWriteTime(numbersVector))//耗费 8457.379 纳秒

我们看到,list和vector在updated 操作上,有着近三百多倍的差距,而且差距会随着规模的增大而继续拉大

Tuples and Maps

和List一样,Tuple也是不可变的,但是和list也有不同之处:元组可以包含不同类型的元素,但是list为元素为统一中类型

Tuples

声明元组的语法如下:

1
2
3
val aTuple = new Tuple2(2,"hello scala")//最正规也最复杂的定义
val aTuple = Tuple2(2,"hello scala") //可以把new去掉,因为Tuple2中有apple方法
val aTuple = (2, "hello, Scala") // 这是一种语法糖,等价于用Tuple2来定义

事实上,scala会自动根据元组里面的元素类型以及个数去创建 TuplesN[],N最大为22(因为是根据FunctionN来的),所以说元组的最大容量只有22

  • tuple 访问元素的方法

tuple访问里面的第一个元素用._1; 第二个元素用._2 ,以此类推

如果想要修改tuple中的元素,需要使用copy方法

1
2
3
println(aTuple._1)  // 2
println(aTuple.copy(_2 = "goodbye Java")) //(2,goodbye Java)
println(aTuple.swap) // ("hello, Scala", 2) 只有Tuple2有这个swap方法,其他都没有

Maps

  • Maps是键值对集合,可以由如下定义:
1
2
3
4
5
// string 是key, Int 是value
val aMap: Map[String, Int] = Map()
// 键值对可以使一个Tuple2,也可以是 key -> value形式的(语法糖)
// withDefaultValue(-1)的意思是,如果查到Map中没有的键,就返回-1;若不设置,遇到的时候会报错
val phonebook = Map(("Jim", 555), "Daniel" -> 789, ("JIM", 9000)).withDefaultValue(-1)
  • 判断map是否包含某个键
1
2
println(phonebook.contains("Jim"))// true
print(phonebook("Mary"))//-1,因为 Mary这个键不存在
  • 由于map是immutable的,因此如果要往原来的map里插入新键值对,
1
2
3
val newPairing = "Mary" -> 678
val newPhonebook = phonebook + newPairing
println(newPhonebook)
  • 让tuples打印得更漂亮
1
println(tuple.toString)
functionals on maps
  • map 函数
1
println(phonebook.map(pair => pair._1.toLowerCase -> pair._2))
  • filter函数
1
println(phonebook.view.filterKeys(x => x.startsWith("J")).toMap)
  • mapValues
1
println(phonebook.view.mapValues(number => "0245-" + number).toMap)
map与其结构之间的转换
1
2
3
4
5
// conversions to other collections
// map转换为List
println(phonebook.toList)
// List转换为Map
println(List(("Daniel", 555)).toMap)
groupBy函数

groupBy函数比较有用,可以将List中的元素分成组。比如说

1
2
3
4
5
val names = List("Bob", "James", "Angela", "Mary", "Daniel", "Jim")
println(names.groupBy(name => name.charAt(0)))
//HashMap(J -> List(James, Jim), A -> List(Angela), M -> List(Mary), B -> List(Bob), D -> List(Daniel))
println(names.groupBy(name => name.charAt(0) == 'J'))
//HashMap(false -> List(Bob, Angela, Mary, Daniel), true -> List(James, Jim))

Exercise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
1. What would happen if I had two original entries "Jim" -> 555 and "JIM" -> 900
!!! careful with mapping keys.

2. Overly simplified social network based on maps
Person = String
- add a person to the network
- remove
- friend (mutual)
- unfriend

- number of friends of a person
- person with most friends
- how many people have NO friends
- if there is a social connection between two people (direct or not)
*/

Options

现在来介绍一下Scala中一种特殊的类: Option

Option 可以看做一个容器要么有东西(Some),要么什么东西都没有。我们可将其看成一个长度为0或1的List。当Option里面有东西的时候,这个List的长度是1(也就是 Some),而当你的Option里没有东西的时候,它的长度是0(也就是 None)。

那么Option可以应用在哪里?我们设想一个情况:打印一个还未分配内存的字符串,显然,这会导致 Null Pointer Error并使得程序崩溃。

1
2
val string: String = null
println(string.length)

为了解决这个问题,正常方法可以这样写,但是如果通篇都是这样的处理,就会显得很杂乱

1
2
3
4
val string : String = null
if (string != null) {
println(string.length)
}

此时,Option就发挥其作用了。Option可以作为一个wrapper,被它包含的值可以使存在的,也可以是Nothing。

1
2
3
4
def unsafeMethod(): String = null
// val result = Some(null) // WRONG
val result = Option(unsafeMethod()) // Some or None
println(result) //None

此外,在 map 中,我们可以用Option来包裹 map.get("key") 如果key不存在,那么值就是 None

又比如,在访问一个空列表的头部的时候,也可以用Option。

我的理解是,option有点像植物大战僵尸中的南瓜,南瓜里面可以有植物,也可以什么都没有,但南瓜放在那边就会起一个保护作用。

使用 getOrElse() 方法

以下是示例程序,显示了如何使用getOrElse()方法访问值或不存在值时的默认值。

比如说:

1
2
3
def backupMethod(): String = "A valid result"
// 如果前面函数的调用值是None,那么就会调用后面的函数
val chainedResult = Option(unsafeMethod()).orElse(Option(backupMethod()))

事实上,我们可以在进行优化,在定义函数的时候,就将返回值定为 Option, 这样可读性更强,而且对用户更友好(api中已经设定了option,用户不用自己再套一层option)

1
2
3
def betterUnsafeMethod(): Option[String] = None
def betterBackupMethod(): Option[String] = Some("A valid result")
val betterChainedResult = betterUnsafeMethod() orElse betterBackupMethod()

functions on Options

1
2
println(myFirstOption.isEmpty) // 判断是否为空
println(myFirstOption.get) // 获得Option中的值,由于可能是空值,因此不安全

Option也可以使用map、filter和flatMap函数

1
2
3
4
val myFirstOption: Option[Int] = Some(4)
println(myFirstOption.map(_ * 2)) // Some(8)
println(myFirstOption.filter(x => x > 10))// None
println(myFirstOption.flatMap(x => Option(x * 10)))// Some(40)

for-comprehensions

在设计api的时候,如果返回值可能是None,我们需要在前面套一层Option使得api更加安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  class Connection {
def connect = "Connected" // connect to some server
}
object Connection {
val random = new Random(System.nanoTime())

def apply(host: String, port: String): Option[Connection] =
if (random.nextBoolean()) Some(new Connection)
else None
}

val forConnectionStatus = for {
host <- config.get("host")
port <- config.get("port")
connection <- Connection(host, port)
} yield connection.connect

forConnectionStatus.foreach(println)

Handling Failure

Pattern Matching

Pattern Matching

pattern matching是scala中很重要的一部分,它类似于switch case,可以对一个值进行条件判断,然后针对不同的条件进行不同的处理。

但是Scala的模式匹配的功能比Java的swich case语法的功能要强大的多,Java的swich case语法只能对值进行匹配。但是Scala的模式匹配除了可以对值进行匹配之外,还可以对类型进行匹配、对Array和List的元素情况进行匹配、对case class进行匹配、甚至对有值或没值(Option)进行匹配

一个常见的pattern match的语法如下:很容易理解,就是swich case

1
2
3
4
5
6
7
8
9
val random = new Random
val x = random.nextInt(10)

val description = x match {
case 1 => "the ONE"
case 2 => "double or nothing"
case 3 => "third time is the charm"
case _ => "something else" // _ = WILDCARD
}

在使用pattern matching的时候我们要注意几点:

  • case 要按照一定的顺序来组织,增强可读性
  • 为了防止出现 MatchError的情况,一定要设置 Wildcard,也就是默认不匹配情况下的返回值。(用 _符号)

pattern matching还有其他好用的特性

解耦合

模式匹配不单匹配值,甚至可以匹配类中的某个成员变量来进行条件筛选

1
2
3
4
5
6
7
8
9
case class Person(name: String, age: Int)
val bob = Person("Bob", 20)

val greeting = bob match {
case Person(n, a) if a < 21 => s"Hi, my name is $n and I can't drink in the US"
case Person(n, a) => s"Hi, my name is $n and I am $a years old"
case _ => "I don't know who I am"
}
println(greeting)

PM在继承类中的应用

Pattern Matching 甚至可以匹配子类类型。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  sealed class Animal
case class Dog(breed: String) extends Animal
case class Parrot(greeting: String) extends Animal
case class Cat(meow: String) extends Animal

val animal: Animal = Dog("Terra Nova")
val animal2: Animal = Parrot("Terra Nova")
val animal3: Animal = Cat("Meow Meow")

animal3 match {
case Dog(someBreed) => println(s"Matched a dog of the $someBreed breed")
case Parrot(someGreetings) => println(s"Match a parrot of $someGreetings greeting")
case _ => println("Something else")
}

Exercise

给出如下要求:

1
2
3
4
5
6
7
8
9
10
/*
Exercise
simple function uses PM
takes an Expr => human readable form

Sum(Number(2), Number(3)) => 2 + 3
Sum(Number(2), Number(3), Number(4)) => 2 + 3 + 4
Prod(Sum(Number(2), Number(1)), Number(3)) = (2 + 1) * 3
Sum(Prod(Number(2), Number(1)), Number(3)) = 2 * 1 + 3
*/

要我们用PM实现代码到数学公式的转换。这里最难想到的就是乘法的处理:

  • Prod(Number(2), Number(1)) , 对2和1不需要改,直接变成 2*1
  • Prod(Sum(Number(2),Number(1)),Number3),那么前面的Sum(Number(2),Number(1)) 就需要额外加个括号,符合预算规则
  • Prod(Prod(Number(2),Number(1)),Number(3)) ,那么不需要括号,计算出2*1之后在和3计算得到2*1*3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
trait Expr
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr
case class Prod(e1: Expr, e2: Expr) extends Expr

def show(e: Expr): String = e match {
case Number(n) => s"$n"
case Sum(e1,e2) => show(e1)+" + "+show(e2)
case Prod(e1,e2) => {
def matchInProdExp(e: Expr): String = e match {
case Number(n) => show(e)
case Prod(e1,e2) => show(e)
case _ => "("+ show(e)+")"
}
matchInProdExp(e1)+" * "+matchInProdExp(e2)
}
}

测试:

1
2
3
4
5
println(show(Sum(Number(2), Number(3))))
println(show(Sum(Sum(Number(2), Number(3)), Number(4))))
println(show(Prod(Sum(Number(2), Number(1)), Number(3))))
println(show(Prod(Sum(Number(2), Number(1)), Sum(Number(3), Number(4)))))
println(show(Sum(Prod(Number(2), Number(1)), Number(3))))

ALL the Patterns

这一节我们来系统总结一下总共有多少Patterns可以供我们去匹配

constants

case里面可以是很多类型,数字、字符串、布尔值、对象等

1
2
3
4
5
6
7
val x: Any = "Scala"
val constants = x match {
case 1 => "a number"
case "Scala" => "THE Scala"
case true => "The Truth"
case AllThePatterns => "A singleton object"
}

variables

1
2
3
val matchAVariable = x match {
case something => s"I've found $something"
}

wildcard

1
2
3
val matchAnything = x match {
case _ =>
}

tuples

1
2
3
4
5
val aTuple = (1,2)
val matchATuple = aTuple match {
case (1, 1) =>
case (something, 2) => s"I've found $something"
}

甚至可以匹配 嵌套元组:

1
2
3
4
val nestedTuple = (1, (2, 3))
val matchANestedTuple = nestedTuple match {
case (_, (2, variable)) =>
}

case classes

constructor pattern可以匹配我们自己创建的case class。比如说我创建了一个MyList[Int]类型的列表。case类型可以类似于case class的构造器,用来解构类中的成员

1
2
3
4
5
6
7
8
val aList: MyList[Int] = Cons(1, Cons(2, Empty))

val matchAList = aList match {
case Empty => "Empty"
case Cons(head, Cons(subhead, subtail)) => s"head is $head, subhead is $subhead, subtail is $subtail"
}

println(matchAList)//head is 1, subhead is 2, subtail is []

list

List patterns非常有用,它可以有很多种形式的case

  • case List(1, _, _, _) 匹配开头为1,长度为4的List
  • case List(1, _*) 匹配开头为1,长度不限的List
  • case 1 :: List(_) 匹配开头为1的List
  • case List(1,2,_) :+ 42 匹配以42结束的List
  • case h :: t => h + "" + processList(t) 提取list的头元素和尾元素
1
2
3
4
5
6
7
8
val aStandardList = List(1,2,3,42)
val standardListMatching = aStandardList match {
case List(1, _, _, _) => // extractor - advanced
case List(1, _*) => // list of arbitrary length - advanced
case 1 :: List(_) => // infix pattern
case List(1,2,_) :+ 42 => "lala"// infix pattern
case h :: t => h + "" + processList(t) //haskell-like prepending
}

type

scala还可以匹配输入对象的类型,如下:

1
2
3
4
5
6
7
val unknown: Any = 2
val unknownMatch = unknown match {
case list: List[Int] => // explicit type specifier
case _:String => "this method returns String"
case _:Int => "this method returns Int"
case _ => "this method returns something else"
}

name binding

1
2
3
4
val nameBindingMatch = aList match {
case nonEmptyList @ Cons(_, _) => // name binding => use the name later(here)
case Cons(1, rest @ Cons(2, _)) => // name binding inside nested patterns
}

multi-patterns

multiple patterns就是将两个模式用 Pipe符号连接起来——只要符合其中一个模式,就匹配成功,如下:

1
2
3
4
// 8 - multi-patterns
val multipattern = aList match {
case Empty | Cons(0, _) => // compound pattern (multi-pattern)
}

if guards

可在模式后添加 if 语句

1
2
3
val secondElementSpecial = aList match {
case Cons(_, Cons(specialElement, _)) if specialElement % 2 == 0 =>
}

注明

JVM中会存在type erase的情况,如下:

此时打印 numbersMath,会得到 a list of strings,这是因为在 Java刚开始被创建时,是没有泛型这一概念的,泛型直到Java5才被加入。因此,JVM在做类型判断的时候,为了能让Java1的程序也能运行,在判断的时候,将泛型全部抹去了

因此,在java内部,事实上不会对List究竟是哪一种类型的做匹配,而是只匹配是否为List

1
2
3
4
5
6
7
8
val numbers: List[Int] = List(1, 2, 3)
val numbersMatch = numbers match {
case listOfStrings: List[String] => "a list of strings"
case listOfNumbers: List[Int] => "a list of numbers"
case _ => ""
}

println(numbersMatch)

Patterns Everywhere

事实上,模式匹配的思想在 scala中随处可见:

在try-catch中

1
2
3
4
5
6
7
try {
// code
} catch {
case e: RuntimeException => "runtime"
case npe: NullPointerException => "npe"
case _ => "something else"
}

在try-catch中的case 其实是简写的形式,事实上在catch里面也有一个match,如下

1
2
3
4
5
6
7
8
9
try {
// code
} catch (e) {
e match {
case e: RuntimeException => "runtime"
case npe: NullPointerException => "npe"
case _ => "something else"
}
}

在 for 中

1
2
3
4
5
6
7
8
9
10
val list = List(1,2,3,4)
val evenOnes = for {
x <- list if x % 2 == 0 //也是一种模式匹配
} yield 10 * x

// generators are also based on PATTERN MATCHING
val tuples = List((1,2), (3,4))
val filterTuples = for {
(first, second) <- tuples
} yield first * second

Tuple, List

可以直接提取出tuple、list中的元素,这也包含了PM的思想。

1
2
3
4
5
6
7
val tuple = (1,2,3)
val (a, b, c) = tuple
println(b)

val head :: tail = list
println(head)
println(tail)

partial function

有时我们会遇到这样子的函数,可能会被搞得不知道是什么意思,其实这也是PM的简化形式

1
2
3
4
5
6
val list = List(1,2,3,4)
val mappedList = list.map {
case v if v % 2 == 0 => v + " is even"
case 1 => "the one"
case _ => "something else"
} // partial function literal

省去了对每一个list中的元素x做match的过程

1
2
3
4
5
6
val mappedList2 = list.map { x => x match {
case v if v % 2 == 0 => v + " is even"
case 1 => "the one"
case _ => "something else"
}
}
-------------本文结束,感谢您的阅读-------------