0%

【Scala 实用指南】跑步起步

这本书比较适合有一定 Java 基础,想要快速转型 Scala 或使用 Scala 编写代码的同学。


更新历史

  • 2020.02.18:重新上线
  • 2019.03.20:完成全本阅读,完成读后感,变为参考书
  • 2019.03.18:完成第一部分,和 Actor 部分,剩下的有时间继续深入了解
  • 2019.03.17:开始阅读

读后感

这本书比较适合有一定 Java 基础,想要快速转型 Scala 或使用 Scala 编写代码的同学。对我这种一直写 golang,对 Java 缺乏大型项目经验的人来说,会稍微有点难,但是越过了理解的门槛之后,很多道理是相通的。至少看完这本书,接手现在手头上的 DSP 代码没有问题。不过在学习的过程中,也暴露了自己的一些知识的盲区,是以后需要补上的,具体如下:

  • 加深对 JVM 上代码运行机制的理解
  • 系统学习 Scala 的语法
  • 复习 Java 的语法
  • 熟悉 JavaBean 等规范
  • 用 Akka 编写项目

阅读笔记

序与前言

Scala 的美在于精巧的内核,Scala 的丑陋在于复杂的实现。从实际工作的角度,推崇编写贴近 Java 风格的 Scala 代码,并适度地利用 Scala 的语言特性简化代码。

Akka 是一个用 Scala 构建的卓越类库,用于创建具有高即时响应性、并发性的反应式应用程序。

本书用的 Scala 代码是 2.12.6

第 1 章 探索 Scala

Scala 是一门强大的编程语言:不需要牺牲强大的静态类型检查支持,就可以写出浮游变现力而又简洁的代码。关键特性:

  • 同时支持命令行式风格和函数式风格
  • 纯面向对象
  • 强制合理的静态类型和类型推断
  • 简洁而富有表现力
  • 能和 Java 无缝地互操作
  • 基于精小的内核构建
  • 高度的伸缩性,仅用少量代码就可以创建高性能的应用程序
  • 具有强大、易用的并发模型

安装 Scala

先下载 JDK8(链接),然后在 Mac 下直接 brew install sbt; brew install scala 即可。

第 2 章 体验 Scala

输入 scala 可以进入 REPL 模式,以交互的方式查看代码运行的结果。在输入时,ctrl+A 转到行首,ctrl+E 转到行尾。

第 3 章 从 Java 到 Scala

  • 使用 val 定义的变量是不可变的
  • 使用 var 定义的是可变的(不推荐使用)
  • 1 to 3 等价于 1.to(3),但看起来更简洁
  • to 包含下界与上界,until 则不包含上界
  • 如果方法没有参数或只有一个参数,可以省略点号和括号。如果有多个参数,则必须使用括号。
  • 所有的类型都视为对象
  • 支持元组与多重赋值
  • 如果 info 是一个元组,那么 info._1 可以访问其中第一个元素,第二个是 info._2
  • 元组不仅可以用于多重赋值。在并发编程时,Actor 之间也将元组以数据值列表的形式作为消息进行传递。
  • 只有最后一个参数可以接受变长参数值,可以传递零个、一个或者多个参数,方式是在参数类型后面加上 *
  • 数组展开标记 _*
  • 支持设置参数默认值 def mail(destination: String = "head office", mailClass: String = "first")
  • 支持使用命名参数 mail(mailClass = "Priority")
  • 隐式参数需要把参数标记为 implicit,相当于有了默认参数,如果没有传值,Scala 会在调用的作用域中寻找一个隐式变量。(这个特性蛮有意思)
  • 多行字符串 """...""" 即可
  • 字符串插值 s"",用 $ 来引用变量
  • 格式化插值 f"", 比如 println(f"$$${price * discount/100.00}%2.2f")
  • return 是可选的。假定最后一个求值的表达式能够匹配方法所声明的返回类型,则无需显式 return(也不推荐用显式的 return
  • 类和方法默认就是公开的
  • Scala 中赋值操作的结果值是一个 Unit,大概等价于一个 Void
  • Scala 对 == 的处理和 Java 不同,对所有的类型都是一致的
  • protected 让所修饰的成员仅对自己和派生类可见(具体这里还有很多门道,这里不深入)

第 4 章 处理对象

  • 如果类定义没有主体,就没有必要使用大括号
  • 辅助构造器 this 的第一行有效语句必须调用主构造器或者其他辅助构造器
  • 通过在变量前添加 @BeanProperty 可以生成与 JavaBean 兼容的代码
  • 别名 type Cop = PloiceOfficer
  • 方法的重载必须用 override 关键字
  • 只有猪构造器能传递参数给基类的构造器
  • 支持泛型 def echo[T](input1: T, input2: T): Unit
  • 通过 object 关键词创建一个单例对象
  • 同一个名字,可以用 class 创建一个类,同时可以用 object 来创建一个伴生对象(companion object),它们共享作用域,提供一些类层面的遍历方法
  • Scala 中没有 static 关键字,可以通过在伴生类中建立方法来作为类方法。
  • 包中可以有变量和函数,都被放在一个称为包对象(package object)的特殊单例对象中。

第二部分 深入了解 Scala


第 5 章 善用类型

  • Any 类型是所有类型的超类型,定义了如下方法:!=(), ==(), asInstanceOf(), equals()hashCode(), isInstanceOf(), toString()
  • Any 类型的直接后裔是 AnyValAnyRef 类型。AnyVal 是 Scala 中所有值类型的基础类型,并映射到 Java 中的原始类型,但 AnyRef 包含了 Java 的 Object 方。
  • Nothing 是一切类型的子类型,是一个纯粹的辅助类型,用于类型推断及类型验证
  • Option 类型可以指定打算不返回结果,比如 None,可以针对返回结果可能存在也可能不存在的情况
  • Either 类型,从一个函数中返回两种不同类型的值之一,Left 表示错误,Right 表示正确
  • 在函数声明和它的主体之间使用等号是理想的惯用风格,这样可以进行类型推断
  • 隐式类型转换可以做很多骚操作,但会增加学习成本

第 6 章 函数值和闭包

  • 可以将其他函数作为参数的函数称为高阶函数。高阶函数能减少代码重复,提高代码复用性,简化代码。
  • Scala 用 _ 来表示一个函数值的参数,第一个 _ 表示第一个参数,第二个 _ 表示第二个参数
  • 部分应用函数,绑定部分参数并将剩下的参数留到以后填写,比如 val logWithDateBound = log(date, _: String),而后直接 logWithDateBound("message1")
  • 闭包目前用得少,暂时略过

第 7 章 特质 Trait

特质类似于带有部分实现的接口,提供了一种介于单继承和多继承的中间能力,因为可以将它们混入或包含到其他类中。

  • 如果没有继承类,那么直接用 extends,如果已经继承了,就用 with
  • 类似于 go 的 interface,可以借此实现鸭子类型
  • 特质要求混入了它们的类去实现在特质中已经声明但尚未初始化的变量
  • 不仅可以为类混入特质,还可以为某个具体的实例混入
  • 可以用特质实现装饰器模式

第 8 章 集合

  • List - 有序的对象集合
  • Set - 无序的集合
  • Map - 键值对字典
  • 不可变集合是线程安全的
  • 对于 Map 可以用 filterKeys 方法来过滤,比如 feeds filterKeys (_ startsWith "D")
  • 将一个元素放在当前 List 的前面可以用 :: 方法,比如 a :: list
  • 连接 list,list ::: listA,其中 list 会在 listA 之前
  • foldLeft() 方法将从列表的左侧开始,为列表中的每个元素调用给定的函数值(代码块),foldRight() 则是从右侧开始
  • yield 对每个元素进行相同的操作,和 for 配合使用
1
2
3
4
5
val result = for (i <- 1 to 10)
yield i * 2

val doubleEven = for (i <- 1 to 10; if i % 2 == 0)
yield i * 2

第 9 章 模式匹配和正则表达式

  • 在并发编程中,在 Actor 接收到消息时,会大量使用模式匹配。
  • 可以匹配字面量和常量,使用通配符匹配任意的值、元组和列表、还可以根据类型以及判定守卫来进行匹配。

匹配字面量

1
2
3
4
5
6
def activity(day: String): Unit = {
day match {
case "Sunday" => print("7")
case "Saturday" => print("6")
}
}

匹配通配符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
object DayOfWeek extens Enumeration {
val SUNDAY: DayOfWeek.Value = Value("Sunday")
val MONDAY: DayOfWeek.Value = Value("Monday")
val TUESDAY: DayOfWeek.Value = Value("Tuesday")
val WEDNESDAY: DayOfWeek.Value = Value("Wednesday")
val THURSDAY: DayOfWeek.Value = Value("Thursday")
val FRIDAY: DayOfWeek.Value = Value("Friday")
val SATURDAY: DayOfWeek.Value = Value("Saturday")
}

def activity(day: DayOfWeek.Value): Unit = {
day match {
case DayOfWeek.SUNDAY => println("7")
case DayOfWeek.SATURDAY => print("6")
case _ => println("Code")
}
}

匹配类型和守卫

1
2
3
4
5
6
7
8
9
10
def process(input: Any): Unit = {
input match {
case (_: Int, _: Int) => print("int, int")
case (_: Double, _: Double) => print("double, double")
case msg: Int if msg > 1000000 => println("big msg")
case _: Int => print("int")
case _: String => println("string")
case _ => print("no good $input")
}
}

case 类是特殊的类,可以使用 case 表达式来进行模式匹配。case 类很简洁,并且容易创建,它将其构造参数都公开为值。可以使用 case 类来创建轻量级值对象,或者类名和属性名都富有意义的数据持有者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
trait Trade
case class Sell(stockSymbol: String, quantity: Int) extends Trade
case class Buy(stockSymbol: String, quantity: Int) extends Trade
case class Hedge(stockSymbol: String, quantity: Int) extends Trade

object TradeProcessor {
def processTransaction(request: Trade): Unit = {
request match {
case Sell(stock, 1000) => println(s"Selling 1000-units of $stock")
case Sell(stock, quantity) => println(s"Selling $quantity units of $stock")
case Buy(stock, quantity) if quantity > 2000 => println(s"Buying $quantity (large) units of $stock")
case Buy(stock, quantity) => println(s"Buying $quantity units of $stock")
}
}
}

正则表达式这里略过

无处不在的下划线字符

  • 作为包引入的通配符,import java.util._
  • 元组索引的前缀 names._1names._2
  • 作为函数值的隐式参数 list.map { _ * 2 } 等同于 list.map { e => e * 2 }
  • 用于默认值初始化变量 var min: Int = _
  • 用于在函数名中混合操作符 foo_:
  • 进行模式匹配时作为通配符 case _
  • 处理异常时,在 catch 代码块中和 case 联用
  • 作为分解操作的一部分,如 max(arg: _*) 将数组分解成离散的值
  • 用于部分应用一个函数 val square = Math.pow(_: Int, 2),部分应用 pow() 方法来创建一个 square() 函数

第 10 章 处理异常

抛出异常 throw new IllegalArgumentException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
object Tax {
def taxFor(amount: Double): Double = {
if (amount < 0)
throw new IllegalArgumentException("Amount must be greater than zero")
if (amount < 0.01)
throw new RuntimeException("Amount to small to be taxed")
if (amount > 1000000)
throw new Exception("Amount too large")

amount * 0.08
}
}

// 要注意 catch 的顺序
for (amount <- List(100.0, 0.009, -2.0, 1000001.0)) {
try {
print(s"Amount: $$$amount ")
println(s"Tax: $$${Tax.taxFor(amount)}")
} catch {
case ex: IllegalArgumentException => println(ex.getMessage)
case ex: RuntimeException => println(s"Don't bother reporting... ${ex.getMessage}")
}
}

第 11 章 递归

  • 递归使用栈,栈的大小决定了不能处理嵌套很深的递归。
  • Scala 使用会对尾递归进行优化

第三部分 Scala 中的并发编程


第 12 章 惰性求值和并行集合

可以通过以下两种方式来提高应用程序的即时响应性。一种是使用多线程来更快地进行计算,也就是说,并行地运行多个任务,而不是一个一个地运行;另一种不是更快地运行这些任务,而是明智地运行它们,并将任务的执行尽可能推迟。惰性求值的好处有:

  1. 可以仅仅运行和当前计算相关的任务,稍后再执行其他的任务。
  2. 对于当前计算来说,被推迟的任务如果是不需要的,那就可以节约时间和资源。
  3. 将不可变变量声明为 lazy,Scala 编译器会推迟绑定变量和它的值,直到该值被使用时为止
  4. 惰性变量生成值得计算过程没有任何的副作用,也就是说,它们不会影响任何外部状态,同时也不会受到外部状态的影响。
  5. 可以通过 par 方法来进行并行计算,比如 timeSample { cities => (cities.par map getWeatherData).toList }

第 13 章 使用 Actor 编程

Actor 帮助我们将共享的可变性转换为隔离的可变性(isolated mutability)。Actor 是保证互斥访问的活动对象。没有两个线程会同时处理同一个 Actor。

一个 Actor 也是一个对象,但不会直接调用它的方法,而是通过发送消息,并且每一个 Actor 都由一个消息队列支撑。

Scala 使用来自 Akka 的 Actor 模型支持。Akka 的Actor 托管在一个 ActorSystem 中,它管理了线程、消息队列以及 Actor 的生命周期。使用 actorOf 工厂方法来创建 Actor。

线程之于 Actor 类似于客服经理之于消费者。

  • 更多地依赖无状态的而不是有状态的 Actor。无状态的 Actor 没有特殊性,它们可以提供更多地并发性,易于复制,并且很容易重启和复用。
  • 要保证 receive() 方法中的处理速度非常快,尤其是接收 Actor 具有状态的时候。改变状态的长时间运行任务会降低并发性,要避免这样做。
  • 确保在 Actor 之间传递的消息是不可变的对象。
  • 尽量避免使用 ask() 。双向通信通常都不是一个好主意,“发送并忘记”模型要好得多

第四部分 Scala 实战


第 14 章 和 Java 进行互操作

  • 如果要使用的 Java 类是标准 JDK 的一部分,那么可以直接使用,否则需要导入,并确保将对应字节码所在的 classpath 指定给 scala

第 16 章 单元测试

可以使用 JUnit,也可以使用 ScalaTest。

JUnit 方式

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util
import org.junit.Assert._
import org.junit.Test

class UsingJUnit {
@Test
def listAdd(): Unit = {
val list = new util.ArrayList[String]
list.add("Milk")
list add "Sugar"
assertEquals(2, list.size)
}
}

ScalaTest 方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util
import org.scalatest._

class UsingScalaTest extens FlatSpec with Matchers {
trait EmptyArrayList {
val list = new util.ArrayList[String]
}

"a list" should "be empty on create" in new EmptyArrayList {
list.size should be(0)
}

"a list" should "increase in size upon add" in new EmprtyArrayList {
list.add("Milk")
list add "Sugar"

list.size should be(2)
}
}

可以使用 Mockito 来模拟请求,减少依赖