前言

在梳理 Kafka Server 源码前,参考官方文档和书籍《Scala for the impatient 2ed》来学习 Scala,本文为读书笔记,简要记录各章节重点,仅供个人 Review

开始前,参考 Tour of Scala,Scala 特点如下:

  • object-oriented:纯面向对象,任何值都是对象;小到基础类型变量,大到用户类实例
  • functional:函数式编程;函数本身也是值,可作为实参或返回值,支持高阶函数
  • statically typed:静态类型;编译阶段执行类型检查和推断
  • extensible:可通过隐式类扩展已有类型,可自定义字符串插值器

ch01. 数据类型

Scala 类型系统

1
2
3
4
5
Any     // 任何类型的父类型,定义了 hashCode, equals 等方法
AnyVal // 值类型,由于值都是对象,使用 Unit 类表示空值(void),其单例对象记为 ()
AnyRef // 引用类型,类比 java.lang.Object
Nothing // 所有类型的子类型,无法实例化,常用于标记函数无返回;如 ??? 方法返回 Nothing,调用时抛出 scala.NotImplementError
Null // 所有引用类型的子类型,其单例值为 null

类型转换:值类型的隐式转换是单向的;强制转换需调用对应方法,引用类型需调用asInstanceOf[]

1
2
3
val d: Double = Double.MaxValue - 1
// val i: Int = d // error: type mismatch
val i: Int = d.toInt // 逆向类型转换,溢出为 Int.MaxValue

ch02. 控制结构与函数

在 Java 中,表达式 expression 有值,语句 statement 通常没有,但在 Scala 中都有值(“无值”用 Unit 表示)

条件表达式:if-else 表达式本身有值

1
2
3
4
5
6
7
String res = Math.random() * 100 >= 60 ? "passed" : "try again";           // 替代三元运算符
val res: String = if (Random.nextInt(100) >= 60) "passed" else "try again" // 更具表达力

val v1: Any = if (x < 0) "neg" else -1 // 分支类型不同,则表达式的值的类型是公共父类 Any
var v2: Any = if (x < 0) "neg" // 分支可能不返回值,类型同上
v2 = if (x < 0) "neg" else () // 等效的无用值 ()
v2 = if (x < 0) "neg" else Unit // 此处直接用 Unit 类,实则是 Unit.apply 返回了 ()

块表达式:块中最后一个表达式的值,就是块的值

1
2
3
4
5
6
7
val distance = {val dx = x1-x2; val dy = y1-y2; sqrt(dx*dx + dy*dy)} // 无需函数的多步初始化

int x, y = 1; /* java */
System.out.println(x = 10); // 10 // 初始化 x,赋值表达式有值,与 C 一样

var x, y: AnyVal = 1 // 小区别:x,y 都被初始化为 1 /* scala */
x = y = 10 // y 为 10,但 x 为 () // 赋值语句是“无值”的,类型为 Unit

字符串插值 interpolation:预定义 3 个插值器 f, s, raw

1
2
3
4
5
val name = "Jack"; val age = 18.1
// printf("%f", name) // 运行时抛异常 java.util.IllegalFormatConversionException
println(f"${age + 1.0}%10.2f") // f 插值类型安全,编译期检查类型 // ${} 执行复杂表达式
println(s"1\n2 $age%.2f") // s 插值不解析格式化指令,无异常
println(raw"1\n2") // raw 插值返回原样字符串

循环:Scala 中没有continue语句,虽有 break语句但实则由异常机制实现(有开销)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import scala.util.control.Breaks._
breakable {
while (true) { /*...*/
if (sum >= 20)
break
}
}

/* scala/util/control/Breaks.scala */
class Breaks {
private val breakException = new BreakControl
def breakable(op: => Unit) { // 接受一个无参无返回值的函数
try {
op
} catch {
case ex: BreakControl => // 原理是抛出异常并捕捉,随后忽略,函数结束
if (ex ne breakException) throw ex
}
}
def break(): Nothing = { throw breakException } // break 语句实为方法调用,抛出异常
}

高级 for 循环:可带 guard 强化条件判断,可带 yield 推导出序列集合

1
2
3
4
for (i <- 1 to 3; j <- 1 to 3 if i != j) // if 条件即 guard
print(f"${10*i + j}%3d")

var vector = for (i <- 1 to 10) yield i * 10 // for 推导式 // 受限版的 Python list comprehension..

函数:函数体本身也是语句块,最后一个表达式的值即函数返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
def fac(x: Int): Int = if (x <= 0) 1 else x * fac(x - 1) // 递归函数必须指定返回值类型,编译器才能校验 fac(x-1) 的类型

def wrap(str: String, l: String = "[", r: String = "]"): String = l+str+r // 参数可带默认值
println(wrap("ABC", "<")) // <ABC] // 从左到右覆盖默认参数
println(wrap("ABC", right = ">")) // [ABC> // 使用带名参数(named parameter)

def sum(args: Int*): Int = { // 变长参数使用 * 声明
var res = 0
for (arg <- args) res += arg
res
}
// println(sum(1 to 3)) // type mismatch: found: scala.collection.immutable.Range.Inclusive,required: Int
println(sum(1 to 3: _*)) // 需添加参数注解 : _* 来将序列展开

懒值:初始化被推迟到首次访问时执行,原理是每次访问都会线程安全地检查是否已被初始化

1
lazy val src = scala.io.Source.fromFile("/tmp/non-exist.txt") // 不使用 src,不报错

异常:Scala 的异常也是Throwable子类,但没有 checked 受检异常,都是RuntimeException 之类的 unchecked 异常;throw表达式的类型为 Nothing

1
2
// Double 是 Nothing 的父类
var res: Double = if (v < 0) sqrt(v) else throw new IllegalArgumentException(s"$v is negative")

ch03. 数组

定长数组 Array:无需 new 的快捷初始化底层实现为伴生对象 apply 构建

1
2
3
4
5
6
7
8
9
10
11
12
13
val arr1 = new Array[Int](2) // (0, 0) // 默认值初始化
val arr2 = Array(0, 0) // 等效,原理如下:

object Array {
def apply(x: Int, xs: Int*): Array[Int] = {
val array = new Array[Int](xs.length + 1) // 底层还是 new 创建 Array 对象
array(0) = x
var i = 1
for (x <- xs.iterator) { array(i) = x; i += 1 }
array
}
}
arr2(0) = 1 // 使用 () 访问元素

Scala 数组在 JVM 表示和 Java 一样,都用的 newarray 指令创建数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Main {
val vs = new Array[Int](3)
}

// $ scalac Main.scala && javap -v Main
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #19 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_3
6: newarray int
8: putfield #13 // Field vs:[I // 也是 int[] 整型数组
11: return

变长数组 ArrayBuffer:在尾端 O(1) 高效增删元素,在任意位置 O(N) 增删元素,提供++=等快捷运算符

1
2
val arr = ab.toArray // 互转
val ab2 = arr.toBuffer

数组遍历:使用indices获取正向索引区间,to左闭右闭,until左闭右开,by指定步长;总体类似 Python/Go 的切片操作

1
2
3
val vs = Array[Int](1 to 5: _*)
// for (i <- vs.indices by 2) println(vs(i))
for (i <- 0 until vs.length by 2) println(vs(i)) // 1 3 5

数组转换:使用 yield 交出新数组;以移除数组中所有负数的算法为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 保持非负数,生成新 ArrayBuffer O(N)
arr = ArrayBuffer(1, -2, 3, 4, -3, -1, 5)
var arr2 = for (v <- arr if v >= 0) yield v // 非原地修改

// 2. 收集负数索引,从后向前删除 O(N^2)
arr = ArrayBuffer(1, -2, 3, 4, -3, -1, 5)
val negIdxes = for (i <- arr.indices if arr(i) < 0) yield i
for (negIdx <- negIdxes.reverse) arr.remove(negIdx)

// 3. 收集非负数索引,逐个前移覆盖 O(N)
arr = ArrayBuffer(1, -2, 3, 4, -3, -1, 5)
val positiveIdxes = for (i <- arr.indices if arr(i) >= 0) yield i
for (i <- positiveIdxes.indices) arr(i) = arr(positiveIdxes(i))
arr.trimEnd(arr.length - positiveIdxes.length)

常用算法:min, max, sum, product, count等方法是filter, map, reduce的快捷操作

多维数组:数组的数组,不同维度长度可以不同,如创建两行三列的数组:Array.ofDim[Int](2, 3)


ch04. 映射与元组

Tuple 描述了类型可不同、数量不定的对象集合,若对象数量为 2 即 Tuple2[T1,T2],又名对偶 Pair

1
2
3
4
5
6
7
final case class Tuple2[+T1, +T2](_1: T1, _2: T2) /*...*/ // scala/Tuple2.scala

@deprecated // scala/Predef.scala
object Pair {
def apply[A, B](x: A, y: B) = Tuple2(x, y) // 对偶的底层还是 Tuple2
def unapply[A, B](x: Tuple2[A, B]): Option[Tuple2[A, B]] = Some(x) // 反向提取
}

Pair 可用操作符 ->创建,比如"k1"->"v1"将产出值 ("k1", "v1"),等效的声明方式:

1
2
3
4
val v1: Tuple2[String, Int] = "wuYin" -> 100
val v2: (String, Int) = "wuYin" -> 100
val v3 = ("wuYin", 100)
println(v1 == v2, v2 == v3) // true, true

创建映射:Map 是 Pair 的集合,默认创建不可变 Map,只读不可修改,

1
2
3
4
val m1 = Map("a"->10) // scala.collections.immutable.Map 未定义各种写操作,故实现只读
// m1("a") = 100 // value update is not a member of scala.collection.Map[String,Any]
val m2 = mutable.Map(("a", 10), ("b", "B"))
m2('c') = () // ok

读取映射:()直接访问,若 key 不存在则抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val m = scala.collection.immutable.Map("a" -> 1, "b" -> "B")
try {
m("A") // () 直接访问
} catch {
case e: NoSuchElementException => println(e.getMessage) // key not found: A
}
val opt: Option[Any] = m.get("a")
if (opt.isDefined) println(opt.get.asInstanceOf[Int]) // get 返回 Option 对象

var a = m.getOrElse("A", 10) // if (m.contains("A")) m("A") else 10 等效

val m2 = m.withDefaultValue(10) // 查询 miss 时返回默认值的不可变 Map
println(m2("A")) // 10
println(m2) // Map(a->1, b->B) // 保持只读,与 Python 的 defaultdict 不同

更新映射:Map 重写了-运算符删除 key,+更新 key,对于可变映射则原地修改,不可变映射则生成新映射返回(由于不可变,二者底层仍共享数据,依旧高效)

1
2
3
4
5
val m = mutable.Map("a" -> 1, 'b' -> "B")
m("a") = "A" // update
m("Z") = "z" // insert
m += ("E" -> 'e', "F" -> "f") // batch insert,delete // 对映射做 diff 批量更新、删除
m -= "a" // delete

迭代映射:使用模式匹配拆解 Map 为 Pair 迭代,类比 Python tuple 的 unpacking 操作

1
2
3
4
val m = Map("A" -> 10, "B" -> 20)
println(m.keySet, m.values)

val revMap: Map[Int, String] = for ((k, v) <- m) yield (v, k) // 翻转 map // Map(10->A, 20->B)

元组:使用 _1, _2, ... 等方式访问元素,适合用于多返回值的函数

zip 操作:将集合中的值纵向绑定为元组,与 Python 中的 zip 函数一致


ch05. 类

无参方法:约定会改变对象状态的 mutator 方法调用时带 (),不会改变状态的、无参的方法可省略,像读取字段一样被调用

1
2
3
4
5
6
class Counter {
private var v = 0
def incr(): Unit = v += 1
// def current(): Int = v // current()
def current: Int = v // 强制 getter 不带 ()
}

带 getter, setter 的属性:scalac 会为var可变字段生成 getter,setter 方法

1
2
3
4
5
6
7
8
9
10
11
class Person {
var age = 0
}

// $ scalac Person.scala && javap -p Person
public class Person {
private int age; // 字段本身是 private 私有的
public int age(); // 生成了同名的 getter 方法,都是 public 的(没有显式 private 修饰)
public void age_$eq(int); // age = // 赋值操作的 setter 方法
public Person();
}

统一访问原则(Uniform Access Principle):模块提供的服务通过统一的方式访问,至于底层是存储还是计算,对访问方无需感知;Scala 通过自动生成的 getter/setter 来践行这一原则:

1
2
3
4
5
6
class Person(var privateAge: Int = 0) { // 实际字段为 privateAge,使用方无需感知
def age_=(newAge: Int): Unit = {
privateAge = if (newAge > privateAge) newAge else privateAge
}
def age: Int = privateAge // 调用方依旧认为 Person 对象有 age 字段,可读可写
}

只带 getter 的属性:val常量只读字段只会生成 getter

1
2
3
4
5
6
7
8
9
class Person {
val age = 0
}

public class Person {
private final int age; // val 实现为私有常量
public int age(); // 只有 getter 开放读操作
public Person();
}

对象私有字段:对于 private 字段,生成的 getter/setter 默认都是 private 的,同类对象间依旧可访问,为更细粒度控制权限,可通过 private[this] 限定只能在单个对象内部访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
private val age = 0
}
public class Person {
private final int age;
private int age();
public Person();
}

class Person {
private[this] val age = 0 // 声明为对象私有字段
}
public class Person {
private final int age; // 不再有 getter 方法, 同类对象也不允许读
public Person();
} // otherPerson.age // error: value age is not a member of Person

总结:Scala 中类的字段都是私有的,不过根据 var, val, private 等组合生成对应的 getter/setter 供外部读写

主构造器(Primary Constructor):类在定义时可像函数一样接受参数,被使用的实参会变为同名字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 // 未带 var,val 的普通参数,若被方法使用则提升为对象私有的字段
class PrimaryUser(private var name: String = "unknown", age: Int = -1, score: Double) {
println(s"constructing..., $score") // 和 Java 类的代码块 {} 一样,在类构造时执行
override def toString: String = s"name: $name, age: $age"
}

public class PrimaryUser {
private java.lang.String name;
private final int age; // 提升后等效于 private[this]
private java.lang.String name(); // otherUser.name
private void name_$eq(java.lang.String); // otherUser.name = "..."
public java.lang.String toString();
public PrimaryUser(java.lang.String, int, double);
} // score 未被其他方法使用,和普通方法参数无异

辅助构造器(Auxiliary Constructor):名字为 this 不与类名绑定,必须从主构造器或其他辅助构造器的调用开始

内部类:Scala 中的类是对象私有的,不同于 Java 的内部类从属于外部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Network(val netName: String) { outer => // 即 Network.this
class Member(val memName: String) {
val contacts = new mutable.ArrayBuffer[Member] // 元素只能是当前对象的 Member 对象
// val contacts = new ArrayBuffer[Network#Member] // # 类型投影:任何 Network 对象的 Member 对象

def whereAmI: String = s"${outer.netName}" // 访问外部类数据,Network.this.netName 可被简化
}

def join(newComer: String): Member = new Member(newComer)
}

def main(args: Array[String]): Unit = {
val net1 = new Network("net1")
val net2 = new Network("net2")
val memA = net1.join("A") // A 是 net1 的会员
val memB = net2.join("B") // B 是 net2 的会员

// 解决:1. 使用类型投影;2. 将 Memeber 抽离到 object Network 伴生对象中,与 Java 行为一致
memB.contacts += memA // error: type mismatch; found net1.Member, required net2.Member
}

ch06. 对象

背景:Scala 不支持 static 静态类型,也不支持 enum 枚举

单例对象:object定义单个对象,除了不能定义构造器外,与普通类无异,其构造也是 lazy 的

伴生对象(companion object):在同一文件中,与类同名的 object 即伴生对象,可相互访问私有数据

1
2
3
4
5
6
7
class Account {
val id = Account.nextId() // 间接调用伴生对象的方法
}
object Account {
private var lastId = 1000 // 伴生对象可存储静态类数据
private def nextId(): Int = { lastId += 1; lastId }
}

object 可直接扩展类或 trait:跳过类定义、类实例化,直接构造出对象

1
2
3
4
5
6
7
8
9
10
11
abstract class Action(val desc: String) {
def undo(): Unit
def redo(): Unit
}

object EmptyAction extends Action("do nothing") { // object 扩展场景:可被共享的默认对象
override def undo(): Unit = {}
override def redo(): Unit = {}
}

val actionMap = Map[String, Action](("up", EmptyAction), ("down", EmptyAction))

App 对象:程序入口是某个 object 的main(Array[String]): Unit方法,或继承自 trait App

枚举:object 扩展 Enumeration 类实现

1
2
3
4
5
object Color extends Enumeration {
val Red, Yellow, Blue = Value() // 返回 Enumeration 的内部类 Val 对象实例,其类型为 Color.Value
}
val color: Color.Value = Color.withName("Red") // Color.Value 才是枚举值的类型
for (v <- Color.values) println(s"(${v.id}, $v)") // 枚举值有 id,name 两个属性

ch07. 包

Scala 的包名是相对的,任何人可在任何时候向任何包中添加内容,2 种声明方式

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.sfti2.demo // 文件顶部标记

package com { // 嵌套标记
package sfti2 {
package demo {
/* ... */
}
}
}

package com.sfti2.demo { // chain-package 限制可见性
class sfti2Conflict{} // com.srti2 包下的成员在此不可见
}

包对象:受限于 JVM 规范,package 不能包含函数或变量,由与之对应的 package object 实现,对象名须与子包在同一层级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package outter {
package object com {
val defaultDomain = "google.com" // 包对象
}
package com {
class Google {
var name: String = defaultDomain
}
}
}
// 比如 π 常量就是 package object 实现
package object math {
@inline final val Pi = java.lang.Math.PI /* ... */
}

包导入:

1
2
3
4
5
import scala.collection._ // _ 是包通配符,Java 的 * 在 Scala 中是合法标识符,可被包含到包名名字中

import java.io.{ByteArrayInputStream, File} // 引入少数成员
import java.util.{HashMap => JavaHashMap} // 重命名
import java.util.{HashMap => _, _} // 屏蔽 java.util.HashMap 并导入其他类型

ch08. 继承

方法重写:非抽象方法的重写强制使用 override 修饰符,自动校验方法名、参数类型是否一致

1
2
3
class Employee extends Person {
override def toString: String = "${super.toString}" // 调用父类方法也是 super
}

类型检查与转换

Scala Java
obj.isInstanceOf[C1] obj instanceof C1
obj.asInstanceOf[C1] (C1) obj
classOf[C1] C1.class
1
2
3
class Klass {}
println(null.isInstanceOf[Klass], null.asInstanceOf[Klass]) // false, null // null 是 Null 类单例
println(null.getClass == classOf[Klass]) // false

protected 成员:仅限子类可见,对同一个包下的其他类并不可见,相比 Java 更合逻辑

1
2
3
4
5
class ClassX {
protected var v = 0
}
val x = new ClassX()
x.v = 0 // error: variable v in class ClassX cannot be accessed in ClassX

父类构造:辅助构造器调用有先后顺序,故子类主构造器必须调用父类主构造器,传参完成初始化,精简流程

1
2
class Person(val name: String, var age: Int) {}
class Employee(name: String, age: Int, val department: String) extends Person(name, age) {}

字段重写:子类继承父类后,可重写字段为不同的值;规则如下:

  • def 重写父类 def,即方法重写:合理
  • val 重写 val,或无参 def:后者可视为字段,合理
  • var 不能直接重写 var,只能重写抽象父类的 var:由于继承,子类已拥有父类的 var 字段,再重写就是重新定义同类型、同名字段,无任何意义(error: cannot override a mutable variable)
1
2
3
4
5
6
7
8
9
10
11
12
13
abstract class User {
val version: String = "v0"
def id: Int // 子类需实现生成 id 的方法
}

class Stu extends User {
// override def id: Int = {Stu.idGen += 1; Stu.idGen} // def 重写 def
override val id = 0 // val 重写 def
override val version: String = "v1" // val 重写 val:继承 final 但值被重写
}
object Stu {
var idGen: Int = 10000
}

匿名子类:直接使用定义或重写的代码块,创建匿名子类

1
2
3
4
5
6
class ClassX {}
val objxWithEcho = new ClassX() { // 此处类似类定义,匿名子类先继承父类,然后新增了 func 方法
def echo(): Unit = ()
}
def f(obj: ClassX {def echo(): Unit}): Unit = {} // 匿名子类是有类型的,叫做 structural type
f(objxWithEcho) // 编译通过 // objxWithEcho 的类型是 ClassX{def echo(): Unit}

构造顺序与提前定义(early definition):在 Java 中,父类的构造函数可调用子类方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class SuperX {
int arrLen() { return 10; }
int[] vs = new int[arrLen()]; // 父类初始化时,会调用子类的方法
}

class SubX extends SuperX {
@Override
int arrLen() { return 2; }

public static void main(String[] args) {
System.out.println(new SubX().vs.length); // 2
}
}

以上等效 Scala 代码,由于字段重写,将出现问题:

1
2
3
4
5
6
7
8
9
class SuperX {
val arrLen: Int = 10
val vs: Array[Int] = new Array[Int](arrLen)
}
class SubX extends SuperX {
override val arrLen: Int = 2
}
val subX = new SubX()
println(subX.vs.length) // 0 // 既不是 2,也不是 10

问题:

  1. new SubX()先调用SuperX的默认主构造器,将其 arrLen 字段设为 10,准备初始化数组,调用 arrLen()
  2. arrLen 字段被重写但还没赋值,返回内存零值,导致创建出长度为 0 的数组 vs

解决:提前定义语法,让父类的构造器执行之前,先初始化子类的指定字段(定义语法结构这么随意的吗…)

1
2
3
class SubX extends {override val arrLen:Int = 2} with SuperX {}
val subX = new SubX()
println(subX.vs.length) // 2

对象相等性:默认 AnyRef 的 equals 默认会调用 eq 检查两个引用是否指向同一对象,实现类时可重写 equals 和 hashCode,实现数据逻辑层的相等性判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Item(val name: String, val price: Double) {
final override def equals(other: Any): Boolean = { // final 避免子类继承后无法正确实现相等性判断
other.isInstanceOf[Item] && {
val otherItem = other.asInstanceOf[Item]
otherItem.name == this.name && otherItem.price == this.price
}
}
final override def hashCode(): Int = (name, price).## // .## 为 null 安全的求哈希值方法
}
val item1 = new Item("item1", 10.2)
val item2 = new Item("item1", 10.2)
println(s"${item1 == item2}, ${item1.equals(item2)}") // true
println(s"${item1.eq(item2)}") // false // 指向 2 个不同对象

class Item(val name: String, val price: Double) { // 更好的做法:模式匹配
final override def equals(other: Any): Boolean = other match { // 参数类型仍为 Any
case i: Item => i.name == this.name && i.price == this.price
case _: AnyRef => false
}
}

ch09. 正则表达式

进程控制:可使用scala.sys.process包模拟的 shell 环境执行程序

1
2
3
4
5
6
7
8
9
10
import scala.sys.process._
val res: Int = "echo 123".! // 执行 shell // process.ProcessBuilder 对象调用 ! 方法执行
println(s"exit code: $res") // 0

var resStr = "ls /tmp".!!
println(s"result string: ${resStr.split("\n")(0)}") // 返回执行的输出结果

resStr = {
"echo 124" #| "wc -l" // #| #&& #>> // 使用 ProcessBuilderImpl 模拟的 shell 运算符
}.!!

正则表达式:

1
2
3
4
5
6
7
8
val numRE = "[0-9]+".r
var wordNumWordRE = "\\s[0-9]+\\s".r // "".r 创建对应的 scala.util.matching.Regex 对象
wordNumWordRE = """\s[0-9]+\s""".r // """ 内部字符串不转义,原样输出
for (num <- numRE.findAllIn("99 a, 98 b"))
println(s"match: $num") // 99, 98

val num: Option[String] = numRE.findFirstIn("c1 a 2 b")
if (num.isDefined) println(s"$num") // Some(1)

ch10. trait

多重继承的问题:多个父类中的同名字段、方法会产生冲突

  • Java 策略:限制类单继承,但可实现多个接口(只能包含抽象方法、静态方法、默认方法,不能包含字段)
  • Scala 策略:限制类单继承,但可扩展(mixin)多个 trait(抽象方法、具体方法、字段都可以有,但不能包含构造器)

基本使用:

1
2
3
4
5
6
7
8
9
10
trait Logger {
def log(msg: String): Unit // trait 中未被实现的方法,即抽象方法
def warn(msg: String): Unit = log(s"[WARN] $msg") // 可使用抽象方法
def noOp(msg:String) :Unit = println(msg) // 可实现具体方法,extends/with 扩展后直接使用
}

// 使用 extends 而非 implement,with 将多个 trait 连为一体
class ConsoleLogger extends Logger with Serializable with Cloneable {
def log(msg: String): Unit = println(msg) // 无需 override
}

mixin:扩展 trait 并使用其抽象方法,在实例化时可 with 指定 mixin 具体的实现类

1
2
3
4
5
6
abstract class ClassX extends Logger {
def doLog(): Unit = log("logging...")
}

val x = new ClassX with ConsoleLogger // 构造对象时 mixin trait 的实现类
x.doLog()

trait 分层:当 mixin 多个存在相互调用关系的 trait 时,隐含从后往前的执行关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val sdf = new SimpleDateFormat("yyyy-mm-dd HH:MM:ss")
trait ShortLogger extends ConsoleLogger {
// super 并不固定指向 ConsoleLogger.log,可能取决于 mixin 前置类型
override def log(msg: String): Unit = super.log(s"$msg".substring(0, 4) + "...") // 阶段过长日志
}

trait TimeLogger extends ConsoleLogger {
override def log(msg: String): Unit = super.log(s"[${sdf.format(new Date())}] $msg") // 加入时间
}

val x1 = new AbstractClassX with TimeLogger with ShortLogger // 先截断,再加时间
val x2 = new AbstractClassX with ShortLogger with TimeLogger // 类似责任链模式,但方向相反
x1.log("x1 log") // [2020-04-11 14:09:44] x1 l...
x2.log("x2 log") // [202...

trait 字段:抽象字段扩展后必须重写,具体字段则被子类扩展(受限于 JVM 单继承约束,实际是被直接添加到子类中来模拟扩展,如果与父类字段发生冲突,则编译报错 inherits conflicting)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
trait ShortLogger extends ConsoleLogger {
val minLen = 1 // 具体字段
val maxLen: Int // 抽象字段
abstract override def log(msg: String): Unit = super.log(
if (msg.length <= maxLen) msg else msg.substring(0, maxLen - 3) + "..."
)
}

// class ClassX { val minLen = 0} // 字段发生冲突,不允许多重继承
// class ClassXX extends ClassX with ShortLogger // class ClassXX inherits conflicting members:

class ClassXX extends ShortLogger {
val maxLen = 20 // 抽象字段在普通子类中必须被定义
}

trait 构造顺序

1
2
3
4
5
6
7
class InternalAccount extends Account with TimeLogger with ShortLogger {/*...*/}
// 当实例化 InternalAccount 对象时,构造顺序如下
// 1. 调用父类的构造器:构造 Account
// 由左到右构造 trait
// 2. 先构造 trait 的父类:ConsoleLogger
// 3. 逐个构造 trait:TimeLogger, ShortLogger(父 trait 已被构造)
// 4. 类构造:InternalAccount

初始化 trait 中的字段:trait 和 object 类似,都被限制不能定义构造函数,其他的特性与普通类无差别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
trait FileLogger extends Logger {
val logfile: String
// val out = new PrintWriter(logfile); // 在 FileLogger 被构造时执行
lazy val out = new PrintWriter(logfile); // 2. lazy 构造 out 变量,等到子类使用时才初始化

override def log(msg: String): Unit = {
out.println(msg); out.flush()
}
}

// class Account extends FileLogger("/tmp/x.log") // trait FileLogger is a trait; does not take constructor arguments
abstract class Account extends Logger {}
var acc = new Account with FileLogger {
override val logfile = "/tmp/x.log" // TODO: 匿名子类没问题
}
acc = new {
override val logfile = "/tmp/x.log" // 1. 使用 early definition
} with Account with FileLogger

trait 继承普通类:trait 的父类会自动成为 mixin 该 trait 的类的父类,也即是父类关系传递

1
2
3
4
5
6
7
8
9
10
trait LoggedException extends Exception with ConsoleLogger {
def log(): Unit = log(getMessage)
}

class NoOpException extends LoggedException {
override def getMessage: String = null // NoOpException 被传递继承自 Exception 父类
}
class NoOpException extends IOException with LoggedException { // 和 Java 类的继承链一个意思
override def getMessage: String = null // IOException 已继承自 Exception
}

self-type:当 trait 有父类时,为了约束扩展该 trait 的子类都将该父类视为自己的父类,triat 可声明自己的自身类型

1
2
3
4
5
6
7
trait LoggedException extends ConsoleLogger {
this: Exception =>
def log(): Unit = log(getMessage) // 此 trait 依赖 Exception 父类,在 mixin 时必须提供
}
// self-type ClassX does not conform to LoggedException's selftype LoggedException with Exception
abstract class ClassX extends LoggedException {}
abstract class ClassX extends IOException with LoggedException {} // ok

来自一个新手的吐槽:实现了静态类型安全,牺牲了语法结构的简洁性


ch11. 操作符

中置操作符(infix operator):本质是带 2 个参数的方法,左侧为隐式参数,右侧为显式参数

1
2
3
a infix b // 与 a.infix(b) 等效
1 to 10 // 1.to(10)
1 -> 10 // 1.->(10) // 本质上是 scala/Predef.scala 中定义的名为 -> 的方法

一元操作符(unary operator):只有一个参数的操作符,参数在前则为前置操作符(prefix operator),否则为后置操作符(postfix operator)

1
2
a- // 即 a.-() 方法调用
-a // a.unary_-() 方法调用,方法带 unary_ 前缀 // 注意只有 +,-,!,~ 是合法的前置操作符

示例:

1
2
3
4
5
6
7
8
9
10
class Point(val x: Int, val y: Int) {
def scale(rate: Int): Point = new Point(x * rate, y * rate)

def *(rate: Int): Point = scale(rate)
def unary_- : Point = scale(-1)
}

val p1 = new Point(1, 2)
val p2: Point = p1 * 2 // (2,4) // 2*p1 不行,因为方法是定义在 Point 上的,而不是 Int 上
val p3: Point = -p1 // (-1,-2)

apply, update, unapply 方法:构造对象、更新对象状态、提取对象构造参数表的语法糖

  • apply:对于 f(arg1, arg2…) 的调用语法,若 f 不是函数或方法,则等同于 f.apply(arg1, arg2…)
  • update:若 f(arg1, arg2…, value) 在赋值语句的左侧,则等同于 f(arg1,arg2…) = value,多用于集合更新元素
  • unapply 方法:apply 的反向操作,用于从对象中提取数量固定的构造参数表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Point(var x: Int, var y: Int) {
def update(v: Int): Unit = {x = v; y = v }
}
object Point {
def apply(x: Int, y: Int): Point = new Point(x, y) // 在伴生对象中,apply 常用于接受构造参数,生成对象
def unapply(p: Point): Option[(Int, Int)] = { // unapply 同理,注意返回的是 Option
if (p.x == 0 || p.y == 0) None else Some((p.x, p.y)) // 约定:若不提取值则返回 Boolean
}
}

val p = Point(1, 2) // new Point(1,2)
p() = 100 // p.x, p.y 都更新为 100
p match {
case Point(x, y) => println(x, y) // 调用 unapply 提取出构造参数表 tuple,进行模式匹配
}

unapplySeq 方法:提取长度不定的值序列,提取结果为Option[Seq[A]]

1
2
3
4
5
6
7
8
9
10
11
object Name {
def unapplySeq(fullName: String): Option[Seq[String]] = {
if (fullName.trim == "") None else Some(fullName.trim.split("""\s+"""))
}
}

val beno = "Benicio Monserrate Sanchez"
beno match {
case Name(first, last) => println(s"$first $last") // 尝试提取一系列的值
case Name(first, mid, last) => println(s"$last") // Sanchez
}

ch12. 高阶函数

函数作为值:「function is first-class citizen」,本质是值,与其他数据类型的值一样可作为实参或返回值

1
2
3
// val f1 = ceil // ceil 是 scala.math 包对象下的方法,是 (Double): Double 方法类型,没有箭头
val f1 = ceil _ // Double => Double 函数类型,_ 将方法变为函数
val f2: (Double) => Double = ceil // 上下文明确为函数,_ 可省略

匿名函数:不带名的函数作为值赋给变量,如 val triple = (x: Double) => x * 3

函数作为参数:函数可作为另一高阶函数的参数,也可作为高阶函数的返回值

1
2
3
4
5
6
7
8
def valueAt4(f: Double => Double): Double = f(4) // valueAt4 是高阶函数,计算传入的函数实参为 4 的结果
println(valueAt4(sqrt), valueAt4(-1*_)) // 2,-4 // valueAt4 类型为 ((Double)=>Double): Double

def multiBy(factor: Double): Double => Double = {
(x: Double) => factor * x // multiBy 类型为 (Double)=>((Double)=>Double) ,返回乘积因子固定的函数
}
val multiBy4: (Double) => Double = multiBy(4)
println(multiBy4(100)) // 400

匿名函数参数类型推断:高阶函数已给出函数形参的类型,匿名函数可被简写,scalac 会做类型推断:

1
2
3
4
5
6
7
8
def applyIn4(f: (Double) => Double) = f(4)
val f1 = applyIn4((num: Double) => num * 3)
val f2 = applyIn4(num => num * 3) // 只有一个参数,省略 ()
val f3 = applyIn4(_ * 3) // 参数在 => 右侧只出现一次,可用 _ 替换 // 参数类型已知,才能这么简写

// val f1 = 3 * _ // missing parameter type for expanded function
val f2 = 3 * (_: Double) // 显式给出 _ 的类型
val f3: (Double) => Double = 3 * _ // 给出了函数类型

常用的高阶函数:foreach, map, filter, reduceLeft,可类比 Python 的 functools 包

闭包 closure:和 Java lambda 表达式一样,Scala 匿名函数能捕捉 effective final 的非局部变量,底层实现也是创建匿名类对象,将捕捉的变量值作为其字段值

1
2
3
4
5
6
7
object Main {
def main(args: Array[String]): Unit = {
def multiBy(factor: Double): Double => Double = (x: Double) => factor * x
var factor: Double = 3
val triple = multiBy(factor) // 返回的函数捕捉了值 3,换成引用只要是 val 也会捕捉
}
}

Curring:将接受 (A,B) 两个参数的函数,变为接受参数 A 返回 “以 B 为参数的函数” 的新函数

1
2
3
4
5
// val product = (x: Int, y: Int) => x * y // 求乘积
val product = (x: Int) => ((y: Int) => x * y) // 参数为 x,返回另一个函数
val multiBy2: Int => Int = product(2) // 返回函数 (y:Int) => 2*y
println(multiBy2(5) == product(2)(5)) // true // 注意 product(2)(5) 发生了两次函数调用
// 至此,product 就是柯里化函数

ch13. 集合

不可变集合(图源 docs.scala-lang.org#collections):

可变集合:

Seq:元素存在先后顺序,其中 IndexedSeq 标识可带下标随机访问

  • 不可变序列
    • Range:整数序列,可用 to, until 生成
    • Vector:树结构实现的 ArrayBuffer 不可变版本
    • List:操作符::使用给定的头、尾创建新列表;head 为第一个元素,tail 为剩余元素组成的子列表;单例对象Nil专门标识空列表

Set:元素不重复的元素集

  • 可变集合:HashSet、LinkedHashSet(记住插入顺序)、SortedSet(按排序顺序访问)
  • 不可变集合:同上,scala.collection.immutable 包提供了不可变版本

Map:Pair 的集合,即映射

  • 可变映射:HashMap,LinkedHashMap(记住添加顺序),SortedMap(按键有序访问)
  • 不可变映射:同上

一些集合特有的操作符:

1
2
3
4
5
6
:+, +:  // prepend, append 新元素到 Seq
+, - // 删除指定元素
++, -- // 批量添加、删除
::, ::: // 追加新元素、新列表到现有列表
#:: // 构建对应的 stream
/*...直接查 scala doc,用一个学一个...*/

ch14. 模式匹配

更精简的 switch 实现(等值匹配):

1
2
3
4
5
val base = ch match { // match 表达式也有值
case '' | '+' => 1 // | 条件合并多个 case
case '-' => -1 // 无需显式 break
case _ => 0 // 类比 default 分支,无匹配时候执行,否则抛出 MatchError 异常
}

使用 guard 做批量匹配:

1
2
3
4
5
6
7
8
try {
ch match {
case _ if Character.isDigit(ch) => println(s"$ch digit found") // Boolean 表达式即 guard
case '-' | '+' | '*' | '/' => println("operator found")
}
} catch {
case err: MatchError => err.printStackTrace() // pattern 中的变量,即匹配值
}

类型匹配:比isInstanceOf, asInstanceOf操作符更佳,同时还支持 Array, List, Pair 的解绑提取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
obj match {
// 值的类型匹配
case i: Int => println(s"$i") // 比 isInstanceOf 更好,i 已完成 asInstanceOf[Int] 转换
case d: Double => println(s"got double $d")
case s: String => println(s"got string $s")

// 匹配数组、列表、元组
case Array(0) => println("array only contains 0") // 背后的原理是 Array.unapplySeq
case 0 :: Nil => println("list only contains 0")

case Array(x, y) => println("size 2 array")
case x :: y :: Nil => println("size 2 list")

case Array(0, _*) => println("array start with 0")
case 0 :: tail => println("list start with 0")

case (0, _) => println("pair which first elem is 0")
case (x, 0) => println(s"pair which second elem is 0, first is $x")

case _ => println("other patterns")
}

提取器 extractor:RegExp 对象的 unapplySeq 方法会提取字符串中能匹配的值序列,搭配模式匹配:

1
2
3
4
5
6
val numAndWordRE = "([0-9]+) ([a-zA-Z]+)".r
val num: Any = "100 score" match {
case numAndWordRE(num, word) => { println(s"$word"); num } // 执行提取
case _ => -1
}
println(num) // 100

变量声明、for 表达式与模式匹配:

1
2
3
4
5
6
val (x, y) = (1, 2)          // 由上可知,pattern 可带变量的,可用于批量初始化变量
val (q, r) = BigInt(10) /% 3 // /% 返回对偶

import scala.collection.JavaConverters._
for ((k, "") <- System.getProperties.asScala) // 找出值为空的环境变量
println(k)

样例类 case class:专为模式匹配优化的类,特性如下

  • 构造器参数默认是 val,即 case class 默认是只读的,也可声明为 var 但不推荐
  • 自动生成的伴生对象已实现了 apply, unapply
  • 提供 copy 方法拷贝状态完全一致的对象
1
2
3
4
5
6
7
8
9
10
abstract class Account
case class ChinaCNY(amount: Double) extends Account // 2 种 case class
case class OtherCurrency(amount: Double, unit: String) extends Account
case object Unknown extends Account // 也可以有单例样例对象
val account: Account = new OtherCurrency(100, "€") // apply 自动实例化
account match {
case ChinaCNY(rmb) => println(s"¥${rmb}") // unapply 自动提取
case OtherCurrency(n, unit) => print(s"$unit$amount")
case Unknown => println("unknown account")
}

ch15 注解内容不多,ch16 XML 处理非必需,暂时跳过

ch17. Future

Future:在未来某个时间点能给出结果(或异常)的对象

执行任务:Future 伴生对象中 apply 方法接受一个无参有返回值(类似 Callable)的函数,表示计算过程

1
2
3
4
object Future {
def apply[T](body: =>T)(implicit @deprecatedName('execctx) ec: ExecutionContext): Future[T] =
unit.map(_ => body)
}

返回函数的参数指向某个ExecutionContext线程池,可直接使用全局线程池,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.DurationInt

val f = Future {
Thread.sleep(1000)
throw new Exception("OOM")
}
// Await.result(f, 10.seconds) // OOM 异常会冒泡
Await.ready(f, 10.seconds) // 异常被放入 Failure
val op = f.value // 返回 Option[Try[T]] 对象
if (op.isDefined) { // Option 未完成为 None,已完成为 Some[T]
op.get match { // 对 Try 对象做模式匹配,有 2 种取值
case Success(v) => println(s"ok, got: $v") // 提取就绪结果
case Failure(ex) => println(s"failed: ${ex.getMessage}") // 提取失败异常
}
}

组合多个 Future:直接嵌套 future 回调会导致需处理大量层级结果,代码混乱,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def getData1: Int = { Thread.sleep((math.random() * 1000).toInt); 1 }
def getData2: Int = { Thread.sleep((math.random() * 1000).toInt); 2 }

val f1: Future[Int] = Future(getData1)
val f2: Future[Int] = Future(getData2) // f1,f2 实际在求值时就已经并发执行了
f1.onComplete({
case Success(v1) =>
f2.onComplete({
case Success(v2) => println(s"result: ${v1 + v2}")
case Failure(ex) => println(s"f2 failed: ${ex.getMessage}")
})
case Failure(ex) => println(s"f1 failed: ${ex.getMessage}")
})
Await.ready(f1, 10.seconds)

更便捷的组合方式是,将 Future 想象为带单个值的集合,可使用 map 并发执行多个 future 并组合结果:

1
2
3
4
5
6
7
val f:Future[Int] = f1.map(v1 => v1 + getData2) // getData2 实际在 getData1 后执行

val f: Future[Future[Int]] = f1.map(v1 => f2.map(v2 => v1 + v2)) // 嵌套 map 的返回结果类型也是嵌套的

val f: Future[Int] = f1.flatMap(v1 => f2.map(v2 => v1 + v2)) // 使用 flatMap 将传递结果并展开
val f: Future[Int] = for (v1 <- f1; v2 <- f2) yield v1 + v2 // 更快捷的 for 表示,会翻译为 map 操作
Await.ready(f, 5.seconds)

有序执行 future:future 之间存在结果的强依赖,可使用 def 定义 future,不急于求值执行

1
2
3
def f1: Future[Int] = Future(getData1) // def 定义,f1 是函数不是值,getData1 不会被执行
def f2: Future[Int] = Future(getData2)
val f = for (v1 <- f1; v2 <- f2) yield v1 + v2 // 先执行 f1 再执行 f2

其他 future 变换:

1
2
3
4
5
foreach    // 逐个收集 for 语句中各 future 的执行结果
andThen // 使用上一 future 的结果,传给下一任务,返回新 future
recover // 将 future 的异常结果恢复为成功结果
fallbackTo // 当发生异常时执行另一个 future
failed // 将失败的 Future[T] 转为成功的 Future[Throwable]

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def getDataFailed: Int = {
Thread.sleep((math.random() * 1000).toInt)
throw new RuntimeException("OOM")
}

val f1 = Future(getData1)
val f2 = Future(getData2)
var f = for (v1 <- f1; v2 <- f2) yield v1 + v2
Await.ready(f, 10.seconds)
f.foreach(v => println(s"got result: $v")) // got result: 3

val f3 = Future(getData1)
val f4 = Future(getDataFailed).recover({
case e: RuntimeException => {
println(s"got runtime error: ${e.getMessage}") // got runtime error: OOM
}
})
val f5 = Future(getDataFailed).fallbackTo(Future(getData1))
f = for (v3 <- f3; v4 <- f5) yield v3 + v4
Await.result(f, 10.seconds)
println(f.value.get.asInstanceOf[Success[Int]].get) // 2

#TODO:更多 Future 用法


ch18. 类型参数

泛型类:使用[]来指明类型参数,scalac 会从参数推断处其实际类型

1
2
3
class PairX[T, S](val v1: T, val v2: S)
var pair1 = new PairX[String, Int]("str", 100)
pair1 = new PairX("str", 100) // 等效的省略表示

泛型函数:类型参数放在名称(方法名)后即可

1
def getMid[T](arr: Array[T]): T = arr(arr.length / 2)

类型变量界定:泛型类可有上界限制其为另一种类的子类,可有下界限制限制其为另一种类的超类,类似 Java 泛型中的 <? extends T><? super T>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class UpBoundPair[T <: Comparable[T]](val v1: T, val v2: T) { // 类型上界:限制类型为另一种类型的子类型
def smaller: T = if (v1.compareTo(v2) < 0) v1 else v2
}
val p1 = new UpBoundPair("s1", "s0")
println(p1.smaller) // s0

class User
class Student extends User
class LowBondPair[T](val v1: T, val v2: T) {
def replaceV1[R >: T](newV1: R) = new LowBondPair(newV1, v2) // 类型下界:将类型声明为另一种类型的超类型
}
val p2 = new LowBondPair(new Student, new Student)
p2.replaceV1(new User)
p2.replaceV1(new Student)

协变、逆变:描述类型参数的继承方向,参考 Scala中的协变,逆变,上界,下界等


ch21. 隐式转换与隐式参数

隐式转换:使用 implicit 修饰,带有单个参数,返回另一种类型的函数。比如实现自动将 Double 转回 Int

1
2
3
implicit def double2int(d: Double): Int = d.toInt
val d = 2.1
val i: Int = d // 2 // 不再报错 type mismatch,而是由 double2int 隐式地执行转换

隐式类:implicit 也可修饰类,其主构造器被当做隐式转换函数,只能包含单个参数,比如功能增强的 File 类:

1
2
3
4
5
6
implicit class FileReader(val from: File) {
def read: String = Source.fromFile(from).mkString
}
val r1: FileReader = new FileReader(new File("/tmp/x.txt")) // 显式转换
val r2: FileReader = new File("/tmp/x.txt") // 等效的隐式转换,自动将 File 对象转为 FileReader 类型
println(r.read) // ok

隐式转换的规则,三种转换场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Fraction(val n: Int, val d: Int) {
def *(other: Fraction): Fraction = new Fraction(n * other.n, d * other.d)
}
object Fraction {
implicit def int2fraction(v: Int): Fraction = new Fraction(v, 1) /* 引入方式一:隐式类或函数可放在伴生对象中 */
implicit def fraction2double(f: Fraction): Double = f.n.toDouble / f.d
}

2 * Fraction(1,2) // 1. 表达式类型与预期类型不同时 // 存在 Int.*(Double) 方法,Fraction 自动转为 Double

import Fraction.int2fraction /* 引入方式二:当前作用域中单个标识符代指的隐式类、隐式函数 */
1.n // 2. 对象访问不存在的成员时 // Int 类没有名为 n 的字段或无参方法,自动转为 Fraction 类型

new Fraction(1, 1) * 10 // 3. 调用对象方法,但实参类型与形参不匹配时

隐式参数:函数或方法可将参数标记为 implicit,编译器会从上下文中查找类型匹配的隐式值,作为实参

1
2
3
4
5
6
7
8
9
10
11
12
case class Delimiters(left: String, right: String)
object ChinaCharacters {
implicit val quotes = Delimiters("【", "】")
}

def f2(): Unit = {
import ChinaCharacters._
def quote(msg: String)(implicit d: Delimiters): String = d.left + msg + d.right

println(quote("abc")(Delimiters("<", ">"))) // <abc>
println(quote("abc")) // 【abc】
}

更多晦涩特性直接参考文档:docs.scala-lang.org/zh-cn/tour


总结

Scala 优点很多:相比 Java 做了大量语法瘦身,如类主构造器带的参数直接作为字段,去掉受检异常,大量重载常用类型的操作符等;支持更合理的 OOP 表示,如无接口限制的 trait,与类对象等效的 object 单例等;还支持函数式编程,函数能以更直观的方式传参和返回;还有便捷的模式匹配,便捷提取对象状态、处理异常… 个人认为缺点是语法细碎,虽然背后大多都有合理的设计

纸上学来终觉浅,绝知此事要躬行,之后学习 Kafka Broker 源码,顺带学习 Scala 的一些实践