Scala 学习小结

前言

  最近要改行做大数据相关的东西了,经调研大数据开发的语言还是用 Scala 好,当然 Java 也可以,毕竟都运行在 JVM 上,不过 Java 也有很长时间没用过了,所以对于 Shaun 来说用 Scala 和 Java 的代价是一样的,都需要学习一下,所以决定用对大数据更友好的 Scala。

  以 Martin Odersky 14 年写的「Scala By Example」为参考,虽然是 14 年的,但 Scala 的基本语法还是没变的,就学习本身而言没问题,毕竟不兼容的只是更上层的 API,Shaun 学习用的 Scala 版本为 2.12.12。Alvin Alexander 的「Scala Cookbook, 2nd Edition」预计今年 8 月会出版,到时可能这本书用来入门更好,但 Shaun 不需要系统的学,就简单的能上手写出比较理想的 Scala 代码就行了。

学习篇

第一章:入门基础

HelloWorld

  由于「Scala By Example」第一章没啥内容,也为了在正式写 Scala 之前简单熟悉一下,这里先用「A Scala Tutorial for Java Programmers」简单上手一下,首先写个 HelloWorld,具体代码如下:

1
2
3
4
5
object HelloWorld {
def main(args: Array[String]) {
println("Hello, world!")
}
}

  和 C 语言类似,程序唯一入口函数都是 main 函数,但 Scala 的变量在前,声明的类型在后,相比常规的语言是有点奇怪了,但这种语法规则和 Typescript 一样,所以很容易接受,但其模板的表示就有点奇怪了,Array[String] 表示一个 String 类型的数组,即表示方法为 Array[T],常规的模板方式为 Array<T>T[],def 关键字用来定义一个函数,object 用来表示一个单例类,即在定义类的同时,又创建了一个类的实例。Scala 中没有 static 关键字,需要用 static 修饰的都放在 object 中即可。

调用 Java

Scala 中默认已导入 java.lang 中的全部类,但其它类需要显式导入,以格式化输出本地日期为例:

1
2
3
4
5
6
7
8
9
10
import java.util.{Date, Locale}
import java.text.DateFormat._

object LocalDate {
def main(args: Array[String]) {
val now = new Date
val df = getDateInstance(LONG, Locale.CHINA)
println(df format now) // df format(now)
}
}

  Scala 中的导入和 java 中 import 基本一样,但功能更强大,可以使用 {} 导入部分,也使用 _ 导入全部(java 导入全部为 *,这不一样),当一个函数只有一个参数,可以通过 空格+参数 的形式调用,而不需要使用 括号包裹 的形式。这里采用 val 关键字声明的是常量,而要声明变量需要用 var

对象

Scala 中万物皆对象,一个数字也是一个对象,一个函数也是一个对象,具体如下图:

enter image description here

以简单计时器函数为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
object Timer {
def oncePerSecond(callback: () => Unit) {
while (true) {
callback();
Thread sleep 1000;
}
}

def timeFiles() {
println("time files like an arrow...");
}

def main(args: Array[String]) {
// oncePerSecond(timeFiles);
oncePerSecond(() => {
println("time files like an arrow...");
});
}
}

  这个和 Typescript 函数式编程的用法基本差不多,唯一不同这里声明的函数返回的是 Unit ,这个 Unit 可认为是无返回的函数,大部分情况等同于 void,在 Scala 中真正的没有值指的是 Nothing。

Scala 中同样有类,具体代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Complex(real: Double, imaginary: Double) {
// def re() = real;
// def im() = imaginary;
def re = real;
def im = imaginary;

override def toString(): String = "" + re + (if (im < 0) "" else "+") + im + "i";
}

object ComplexNumbers {
def main(args: Array[String]) {
val c = new Complex(1.2, -3.4);
// println("real part: " + c.re() + " imaginary part: " + c.im());
println(c.toString());
}
}

  在 Scala 中所有类都会继承某个父类,若没有显式声明父类,则默认继承 scala.AnyRef 类,如上面的 Complex 类,若需要覆盖父类的函数,则需要在函数声明前加上 override 关键字。当函数没有参数时,可以不用加括号,在调用时也不用加括号,如上面示例的注释和非注释的代码。

模式匹配与条件类

  接下来用 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
abstract class Tree
case class Sum(l: Tree, r: Tree) extends Tree
case class Var(n: String) extends Tree
case class Const(v: Int) extends Tree

object Expression {
type Environment = String => Int

def eval(t: Tree, env: Environment): Int = t match {
case Sum(l, r) => eval(l, env) + eval(r, env)
case Var(n) => env(n)
case Const(v) => v
}

def derive(t: Tree, v: String): Tree = t match {
case Sum(l, r) => Sum(derive(l, v), derive(r, v))
case Var(n) if (v == n) => Const(1)
case _ => Const(0)
}

def main(args: Array[String]) {
val exp: Tree = Sum(Sum(Var("x"), Var("x")), Sum(Const(7), Var("y")))
val env: Environment = {case "x" => 5 case "y" => 7}
println("Expression: " + exp)
println("Evalution with x=5, y=7: " + eval(exp, env))
println("Derivative relative to x:\n" + derive(exp, "x"))
println("Derivative relative to y:\n" + derive(exp, "y"))
}
}

  该示例主要用来说明两种 case 关键字,分别为:case class 和 ... match case ...,前者可认为是一个结构体,实例化时可以省略 new 关键字,参数有默认的 getter 函数,整个 case class 有默认的 equals 和 hashCode 方法实现,通过这两个方式可实现根据值判断类的两个实例是否相等,而不是通过引用,条件类同样有默认的 toString 方法实现;后者可认为是一种特殊的 switch case ,只不过 case 的判定和执行是函数式的,case class 可直接参与 match case 的判定(判定是不是属于该类)。第 7 行中有个 type 关键字,可认为是定义了一种新的类型(不是数据类型),示例中是函数类型,通过这个 type ,可直接将字符串映射为整型,23 行中将这个 type 与 case 结合使用,定义多个字符串映射多个整型的变量。第 18 行中有个 _ ,这是 scala 中的通配符,不同的语义下表示的含义不同,这里的含义是指,当上面的模式都不匹配时,将执行这个,相当于 switch case 中的 default。

Scala 中的 trait

  简单理解就是 Java 中的 Interface(接口),Scala 中没有 interface 关键字,但是 trait 比 Interface 的功能更多,其中可直接定义属性和方法的实现,Scala 中可通过 trait 来实现多重继承。下面的示例用 trait 简单实现了一个比较接口:

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
trait Ord {
def <(that: Any): Boolean
def <=(that: Any): Boolean = (this < that) || (this == that)
def >(that: Any): Boolean = !(this <= that)
def >=(that: Any): Boolean = !(this < that)
}

class Date(y: Int, m: Int, d: Int) extends Ord {
def year = y
def month = m
def day = d

override def toString(): String = year + "-" + month + "-" + day

override def equals(that: Any): Boolean = {
that.isInstanceOf[Date] && {
val o = that.asInstanceOf[Date]
o.day == day && o.month == month && o.year == year
}
}

def <(that: Any): Boolean = {
if (!that.isInstanceOf[Date]) {
sys.error("cannot compare " + that + " and a Date")
}

val o = that.asInstanceOf[Date]
(year < o.year) || (year == o.year && (month < o.month || (month == o.month && day < o.day)))
}
}

object Comparable {
def main(args: Array[String]) {
val d1 = new Date(2021, 1, 3);
val d2 = new Date(2021, 1, 3);

println(d1 < d2)
println(d1 <= d2)
}
}

  比较关系一般只需要确定 小于 和 等于 关系即可,其它关系都可由这两关系推出来,由于等于方法默认存在于所有对象中,所以只需要重写小于即可, 其它的比较方法都可以在 trait 中定义好。在上面的示例中有两个函数 isInstanceOf 和 asInstanceOf,前者用来判断对象是否是指定类型,后者用来将对象转换为指定类型,一般用在将父类转为子类时,在使用 asInstanceOf 之前一般需要先使用 isInstanceOf。

泛型

  这东西没啥好说的,基本有编程经验的或见过或用过,只是 Scala 的泛型语法确实有点奇怪就是了,可能也是为了函数式那些乱七八糟的操作符,具体示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Reference[T] {
private var contents: T = _
def set(value: T) {
contents = value
}
def get: T = contents
}

object IntegerReference {
def main(args: Array[String]) {
val cell = new Reference[Int]
cell.set(13)
println("Reference contains the half of " + (cell.get * 2))
}
}

  这里同样有个 _,这里表示的是默认值,对于数字类型来说是 0,对于 boolean 来说是 false,对于 Unit(函数签名)来说是()(无参数无返回),对于其他来说是 null。

简单的了解 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
object QuickSort {
def qSort(xs: Array[Int]) {
def swap(i: Int, j: Int) {
val t = xs(i); xs(i) = xs(j); xs(j) = t;
}

def sort(l: Int, r: Int) {
val pivot = xs(l);
var i = l+1; var j = r;
while (i < j) {
while (i <= r && xs(i) < pivot) i += 1;
while (j > l && xs(j) > pivot) j -= 1;

if (i < j) {
swap(i, j);
i += 1;
j -= 1;
}

if (i > j) {
i = j;
}
}
while (i > l && xs(i) > pivot) {
i -= 1; j -= 1;
}
swap(i, l);

if (l < j-1) sort(l, j-1);
if (j+1 < r) sort(j+1, r);
}

sort(0, xs.length-1);
}

def main(args: Array[String]) {
// val xs = Array(4, 1, 2, 5, 6);
// val xs = Array(1, 2, 4, 4, 55, 5, 6);
// val xs = Array(55, 6, 6);
val xs = Array(4, 1, 5, 7,7,7,7, 2, 6);
qSort(xs);
println(xs.mkString(" "))
}
}

  从这段快排代码可看出,Scala 支持函数嵌套和闭包,即在函数内部定义子函数,子函数可直接使用父函数的变量,同时,这里也简单说明一下 Scala 中数组的一些使用方法,用下标取数组元素时使用的是小括号 (),而不是其它语言常见的中括号 []。当然 Scala 作为一种函数式语言,提供了非常多的函数式操作符,这篇也只会简单介绍。

第三章:Actor

  Actor,Scala 中的多线程编程模型,下方的示例代码在 Scala 2.11 及之后的版本无法运行,因为 Actor 已从 Scala 库独立出来,见 object-actors-is-not-a-member-of-package-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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import scala.actors.Actor

abstract class AuctionMessage
case class Offer(bin: Int, client: Actor) extends AuctionMessage
case class Inquire(client: Actor) extends AuctionMessage

abstract class AuctionReply
case class Status(asked: Int, expire: Date) extends AuctionReply
case object BestOffer extends AuctionReply
case class BeatenOffer(maxBid: Int) extends AuctionReply
case class AuctionConCluded(seller: Actor, client: Actor) extends AuctionReply

case object AuctionFailed extends AuctionReply
case object AuctionOver extends AuctionReply


class Auction(seller: Actor, minBid: Int, closing: Date) extends Actor {
val timeToShutdown = 36000000 // msec
val bidIncrement = 10

def act() {
var maxBid = minBid - bidIncrement
var maxBidder: Actor = null
var running = true

while (running) {
receiveWithin ((closing.getTime() - new Date().getTime())) {
case Offer(bid, client) => {
if (bid >= maxBid + bidIncrement) {
if (maxBid >= minBid) maxBidder ! BeatenOffer(bid)
maxBid = bid; maxBidder = client; client ! BestOffer
} else {
client ! BeatenOffer(maxBid)
}
}
case Inquire(client) => {
client ! BeatenOffer(maxBid)
}
case TIMEOUT => {
if (maxBid >= minBid) {
val reply = AuctionConCluded(seller, maxBidder)
maxBidder ! reply; seller ! reply
} else {
seller ! AuctionFailed
}

receiveWithin(timeToShutdown) {
case Offer(_, client) => client ! AuctionOver
case TIMEOUT => running = false
}
}
}
}
}
}

class HelloActor extends Actor {
def act() {
while (true) {
receive {
case name: String => println("Hello, " + name)
}
}
}
}

object AuctionService {
def main(args: Array[String]) {
val seller: Actor = new HelloActor
val client: Actor = new HelloActor
val minBid = 10
val closing = new Date()

val helloActor = new HelloActor
helloActor.start()
helloActor ! "leo"
}
}

  通过重写 Actor 中的 act 方法即可简单的实现多线程编程,Actor 中有个特殊的标识符 !,该符号其实是是一种缩写,即可将 helloActor.!("leo") 缩写为 helloActor ! "leo",代表将数据传递给 Actor,由 Actor 内部的 receive case 接受数据并处理,当然也可通过 receiveWithin 控制数据传递时间,若超时,则默认触发 TIMEOUT 处理模式。

第四章:表达式与简单函数

该章主要有两个例子:1、牛顿法求平方根;2、尾递归,具体如下:

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
object Sqrt {
def sqrt(x: Double): Double = {
def sqrtIter(guess: Double, x: Double): Double = {
if (isGoodEnough(guess, x)) guess
else sqrtIter(improve(guess, x), x)
}

def improve(guess: Double, x: Double) = {
(guess + x / guess) / 2
}

def isGoodEnough(guess: Double, x: Double) = (guess * guess - x).abs < 0.001 // guess * guess == x

sqrtIter(1.0, x)
}
}

object TailRecursion {
def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)

def facorial(n: Int): Int = if (n == 0) 1 else n * facorial(n-1)

def facorialTail(n: Int): Int = {
def facorialIter(n: Int, res: Int): Int = {
if (n == 0) res
else facorialIter(n-1, res * n)
}

facorialIter(n, 1)
}
}

object SimpleFunc {
def main(args: Array[String]) {
val sqrtValue = Sqrt.sqrt(0.01)
println(sqrtValue)

val gcdValue = TailRecursion.gcd(14,21)
println(gcdValue)

val facorialValue = TailRecursion.facorial(5)
println(facorialValue)

val facorialTailValue = TailRecursion.facorialTail(5)
println(facorialTailValue)
}
}

  由于并没有引入新的语法,就简单聊聊这两个例子吧。牛顿法求平方根主要在于构造一个特殊的二分函数 \(y_{i+1} = (y_i + x / y_i)/2, i=0,1,2,3,..., y_0=1\) ,如此迭代,直到 \(|y_i^2-x| < \epsilon\) ,得到 \(y_i\) 即为 x 的平方根,更朴素一点的求多次方根就是利用二分法,分 [0, 1] 和 [1, +∞] 两个区间即可,对应从 [x, 1] 和 [1, x] 开始二分取值。至于尾递归,以前简单的写过一点,即最后递归调用原函数时,原函数不会再参与任何计算表达式。尾递归的好处在于当编译器或解释器支持尾递归时,将不会产生普通递归时的压栈操作,即不用担心递归层次太深,尾递归将类似循环迭代处理。

第五章:高阶函数

  高阶函数(First-Class Functions),支持以函数作为参数或返回值,也可将函数赋值给其它变量,由此也可引出闭包和柯里化,闭包是指将内嵌函数作为返回值,而柯里化是指将多个参数分解为独立参数传递给函数,如:\(f(args_1,args_2,...,args_n)=f(args_1)(args_2)(...)(args_n)\)。下面以求函数的不动点为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
object FirstClassFunctions {
val tolerance = 0.0001
def isCloseEnough(x: Double, y: Double) = ((x-y) / x).abs < tolerance
def fixedPoint(f: Double => Double)(firstGuess: Double) = {
def iterate(guess: Double): Double = {
val next = f(guess)
if (isCloseEnough(guess, next)) next
else iterate(next)
}
iterate(firstGuess)
}

def averageDamp(f: Double => Double)(x: Double) = (x + f(x)) / 2
def sqrt(x: Double) = fixedPoint(averageDamp(y => x/y))(1.0)

def main(args: Array[String]) {
println(sqrt(0.01));
}
}

  该示例简单明了的展示了 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
// 主构造函数
class Rational(n: Int, d: Int) extends AnyRef {
private def gcd(x: Int, y: Int): Int = {
if (x == 0) y
else if (x < 0) gcd(-x, y)
else if (y < 0) -gcd(x, -y)
else gcd(y % x, x)
}
private val g = gcd(n, d)

// 构造函数重载(辅助构造函数)
def this() {
this(0, 0) // 调用主构造函数
}

val number: Int = if (g != 0) n / g else 0
val denom: Int = if (g != 0) d / g else 0

def +(that: Rational) = new Rational(number * that.denom + that.number * denom, denom * that.denom)
def -(that: Rational) = new Rational(number * that.denom - that.number * denom, denom * that.denom)
def *(that: Rational) = new Rational(number * that.number, denom * that.denom)
def /(that: Rational) = new Rational(number * that.denom, denom * that.number)

def toNumber: Double = if (denom != 0) number.toDouble / denom else 0.0

override def toString = "" + number + "/" + denom
}

object Rational {
def main(args: Array[String]) {
val rational = new Rational(2,1) / new Rational()
println(rational.toNumber);
println(rational.toString);
}
}

  从有理数这个示例可以看出,Scala 的类支持操作符重载,也支持构造函数重载,同样支持继承,多继承也是支持的,每个父类用 with 关键字分隔就行。

第七章:条件类和模式匹配

大致和第一章内容差不多,就不重复写了。

第八章:泛型

  大致也和第一章内容差不多,值得一提的书中实现的泛型栈本质是一个链表,实现方法挺有意思的。通过 <: 标识符可约束泛型的类型,如 [T <: P[T]] 表明泛型 T 必须类型 P 的子类型。而标识符 <%<: 约束性弱一点,只要 T 能够通过隐式类型变换为 P 即可。若想约束为父类型,则需使用 >: 标识符。

  Scala 中有一种特殊的泛型,就是变化型注解,trait List[+T] 代表协变,表示当 B 类型是 A 类型子类时,List[B] 也可认为是 List[A] 的子类;trait List[-T] 代表逆变,当 B 类型是 A 类型子类时,List[B] 可认为是 List[A] 的父类。

  Scala 中同样有元组,使用时也很方便,简单使用直接用括号声明即可,如 def divmod(x: Int, y: Int): (Int, Int) = (x / y, x % y),该函数即返回一个元组,也可声明一个元组 case class Tuple2[A, B](_1: A, _2: B),若需要取元组的元素可通过 _i 的方式,如 val xy = divmod(3, 4); xy._1; xy._2;,也可通过 match-case 语句取,如 xy match { case (n, d) => println("quotient: " + n + ", rest: " + d) }

第九章:List

  Scala 中的 List 其实是数组结构,并且是不可变的,可认为是 C++ 里的静态数组,不能往其中添加或删除元素,下面用数组排序示例下 List 的用法:

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
object Sort {
def insertSort(xsl: List[Int]): List[Int] = {
def insert(x: Int, xs: List[Int]): List[Int] = {
xs match {
// case Nil => List(x)
case List() => List(x)
case y :: ys => if (x <= y) x :: xs else y :: insert(x, ys)
}
}

if (xsl.isEmpty) Nil
else insert(xsl.head, insertSort(xsl.tail))
}

def mergeSort[A](less: (A, A) => Boolean)(xs: List[A]): List[A] = {
def merge(xs1: List[A], xs2: List[A]): List[A] = {
if (xs1.isEmpty) xs2
else if (xs2.isEmpty) xs1
else if (less(xs1.head, xs2.head)) xs1.head :: merge(xs1.tail, xs2)
else xs2.head :: merge(xs1, xs2.tail)
}

val n = xs.length / 2
if (n == 0) xs
else merge(mergeSort(less)(xs take n), mergeSort(less)(xs drop n))
}

def main(args: Array[String]) {
val xs = List(4, 1, 5, 7,7,7,7, 2, 6);
// val xs = 3::2::1::1::Nil;
println(xs(0), xs(1), xs(xs.length-1)) // (4,1,6)
// val ys = insertSort(xs);
val ys = mergeSort((x: Int, y: Int) => x > y)(xs);
println(ys.mkString(" "))
}
}

  List 中有两个操作符非常类似,即 :::::, 前者用于 List 中的元素和 List 连接,即创建一个新 List,新 List 为原 List 头插入元素后的 List,后者用于连接两个 List,即创建一个新 List ,新 List 为将第二个 List 的元素全部放入第一个 List 尾部的 List。字符 Nil 代表空 List 和 List() 等效,head 方法返回 List 的第一个元素,tail 方法返回除第一个元素之外的其它所有元素,还是一个 List,isEmpty 方法当 List 为空时返回 true。List 的 case-match 方法中,case y :: ys 其中 y 代表 xs.head,ys 代表 xs.tail。(xs take n) 表示取 List 前 n 个元素,(xs drop n) 表示取 List 前 n 个元素之外的元素,即与 (xs take n) 取得元素正好互补,而 (xs split n) 返回一个元组,元组中第一个元素为 (xs take n),第二个元素为 (xs drop n)。关于 List 还有些更高阶得方法:filter,map, flatMap, reduceRight, foldRight 等方法就不继续写了。至于动态 List 可用 ListBuffer 结构,当然 Scala 中直接用 Seq 作为返回值和参数一般会更好些。

第十章:序列理解

  Scala 中用来做序列理解的表达式是 For-Comprehensions,具体示例如下:for (p <persons if p.age > 20) yield p.name 相当于 persons filter (p => p.age > 20) map (p => p.name),可以简单认为 for-yield 方法是 filter 和 map 的集合体。下面具体用个 N-皇后(特例是 8 皇后)的示例来具体说明:

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
object NQueen {
def queens(n: Int): List[List[Int]] = {
def isSafe(col: Int, queenList: List[Int], delta: Int): Boolean = {
val curRow = queenList.length-1 + delta
for (row <- List.range(0, queenList.length)) {
val queenCol = queenList(row)
val queenRow = queenList.length-1 - row

if (queenCol == col) return false
if (queenRow == curRow) return false
if ((queenCol - col).abs == (queenRow - curRow).abs) return false
}
true
}

def placeQueens(k: Int): List[List[Int]] = {
if (k == 0) List(List())
else for {
queens <- placeQueens(k-1);
column <- List.range(0, n);
if isSafe(column, queens, 1)
} yield column :: queens
}

placeQueens(n)
}

def main(args: Array[String]) {
val queenList = queens(8);
println("queenCount: " + queenList.length) // 92
}
}

for-yield 表达式中 for 中可以写多条语句,代表多重循环,第 5 行的 for 代表 for 循环,<- 表示取 List 中的元素。


  剩下的几章就没啥特别要写的,重点就两个特性,一个是 Stream ,一个 Lazy,Stream 和 List 有点类似,主要区别在于 Stream 是即时返回的,算一个返回一个,而 List 一般是全部计算完再返回一个 List;Lazy 一般用作常量的修饰符,主要作用是只用该常量被用到时才赋值,否则一直为空,有点类似常见的先判空再取值的封装。

后记

  曾看到过通过刷题去学习新语言的方式,一直都以为很粗暴,但这次照着「Scala By Example」敲下来,感觉还挺有效的,同时也巩固了一下基本的算法知识,后续再把 twitter 的 「Effective Scala」再看一下应该就差不多了。