Scala: Scalable language, 面向对象的函数式编程语言.
类型
类型的首字母大写, 没有基本类型:
- Boolean
- Byte
- Char
- Short
- Int
- Long
- Float
- Double
- String
- Unit (即是 void)
- Any
- AnyRef
- AnyValue
变量
常量: val greeting: String = "Hello world" (value), val π = 3.1415926
变量: var greeting: String = "Hello world" (variable)
名字: 字母, 数字, 特殊操作符如 +, -, *, /, π, θ 等, 不能以数字开头
字符串
val greeting: String = "Hello world"
多行字符串使用 3 个双引号
1
2
3val text = """He is
a good
man"""字符串内插值 (s 开头):
1
val text = s"First Name $firstName, Last Name: ${lastName}"
字符串格式化 (f 开头):
1
2
3// %s10 不足 10 个字符前面用空格补齐
// %-10s, 后面补齐
val text = f"First Name $firstName%3s, Last Name: ${lastName}%10s"反斜杠不转义:
1
val text = raw"Hello \n World" // 也可以使用多行字符串的方式输出 \n, 而不是回车, 多行字符串中也不进行转义
正则表达式
字符串结尾用 .r
(即调用字符串的 r()
函数) 构造 Regex 对象, 由于正则表达式中常有 \
, 所以推荐用不需要转义的字符串来创建正则表达式的对象, 即使用 raw 字符串或者多行字符串的方式:
1 | val date = raw"(\d{4})-(\d{2})-(\d{2})".r |
运算符重载
Scala 里的运算符例如 1 + 2 中的加其实是函数调用 1.+(2)
, 再入 1 to 10
, 1 until 10
, 这几个例子为一个参数的函数调用, 请和下面的无参函数调用进行对比.
无参函数调用
没有参数的函数调用可以如下 3 中方式, 推荐第 1 种方式, 就显示直接访问属性一样:
1 | 3.getClass // 定义时没加 () |
元组
元组是 2 个或者多个
值的有序容器, 提供了一种建立数据结构的通用方法 (函数返回值)
1 | val info = (5, "Alice", true), info._1, info._2 |
表达式
表达式是执行后会返回一个值的代码单元, 可以是单个语句, 也可以是代码块, 函数是命名的表达式, if else, for 循环都是表达式 (while 不是表达式):
- 字面量是表达式, 例如 5, “Alice”, true
- if else 表达式 (没有三元运算符, 可以用 if else 实现):
val max = if (a > b) a else b
- 表达式块 {} 最后一个表达式的值为表达式块的返回值, 可以省略 return
match case
Scala 里没有 switch case, 有与之相似的 match case:
- 不需要
break
- 多个匹配可以使用
|
- 可匹配值, 类型, 正则, 数值范围等
- 可以使用 if 进行条件判断: 模式哨位匹配 pattern guard
- 没有 default, 取而代之的是
other
或者_
单个匹配:
1 | val message = status match { |
多个匹配:
1 | val kind = day match { |
模式哨位匹配 pattern guard:
1 | val result = response match { |
For 循环
For 循环, 使用 <-, to util
和 range, for 循环是表达式, 能用 yield 来产生值:
1 | // Java: for (String item : items) |
默认没有提供 break, 想使用 break 的话需要导入:
1 | import util.control.Breaks.break |
集合
1 | val ns: Array[Int] = new Array[Int](20) // 20 是大小 |
函数和方法
函数是命名表达式:
函数定义形式:
def main(args: Array[String]): Unit = {}
使用表达式块调用函数:
1
2pow(4) // 普通方法调用
pow {2*2} // 先计算 2*2 得到 4, 然后 4 作为函数 pow 的参数进行调用, 省去定义中间变量使用命名参数调用函数:
1
greet(name = "Brown", prefix = "Mr") // 和 OC 的一样, 参数顺序可以和定义的顺序不一致
函数可以有默认参数
1
2
3
4
5tailrec .
def fractional(n: Int, t: Int = 1): Int = {
if (n <= 1) t
else fractional(n-1, t*n)
}函数可以为可变参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// Scala: 类型后面跟上 *
def sum(items: Int*): Int = {
var total = 0
for (item <- items) total += item
total
}
// Java: 类型后面跟上 ...
public static int sum(int... items) {
int total = 0;
for (int item : items) {
total += item;
}
return total;
}函数柯里化 (Currying 咖喱):
把一个参数列表里的参数拆分到多个括号中
1
2
3
4def add(a: Int, b: Int): Int = a + b // add(2, 3)
def add(a: Int)(b: Int): Int = a + b // add(2)(3)
def add(a: Int): Int=>Int = (b: Int) => a + b // 分析: 柯里化的函数相当于函数返回值为函数尾递归函数 (最后一个语句是递归的函数调用), 使用注解
@annotation.tailrec
标记函数为尾递归函数, 不满足的函数会抛出编译错误:1
2
3
4
5
6
7
8
9
10
11
12// 非尾递归函数
def fractional(n: Int): Int = {
if (n <= 1) 1 // 返回 1
else n * fractional(n - 1) // 对递归的结果还进行了计算
}
// 尾递归函数, 结果在结束递归处返回, 并且中间计算结果作为参数传给下一层调用
tailrec .
def fractional(n: Int, t: Int = 1): Int = {
if (n <= 1) t // 返回 t
else fractional(n-1, t*n) // 最后一个语句是递归的函数调用
}函数嵌套定义:
1
2
3
4def max(x: Int, y: Int, z: Int): Int = {
def max(a: Int, b: Int): Int = if (a > b) a else b // 和外层的函数虽然同名, 但参数不同, 不会冲突
max(x, max(y, z))
}函数: 使用 val 和 var 定义, 可以简单的理解为 Lambda, 可以作为方法的参数, 有 2 中定义方式:
1
2
3
4
5
6
7val add: (Int, Int) => Int = (a, b) => a + b // 函数形参常用的方式, 以及 Lambda 提供实现, 也叫 FunctionN(N 最大 22, 参数个数)
val add = (a: Int, b: Int): Int => a + b // 和方法的定义有点像
// Lambda 的使用: 参数为函数
def foo(f: (Int, Int) => Int, a: Int, b: Int): Int = f(a, b) // 有 3 个参数: 第一个为函数, 第二三个为整数
println(foo(_ + _, 3, 4)) // 直接写函数体, 更简洁, 缓存函数的动画计算部分使用这种方式会非常舒服
println(foo((a, b) => a + b, 3, 4)) // 完整形式Scala 函数作为参数比 Java 的 Lambda 直观 (@FunctionalInterface), Scala 直接写函数的签名, 一目了然的看到函数的参数和返回类型, 而 Java 的参数为接口类型, 需要先定义接口, 打开接口的源码或者文档才知道对应方法的签名.
Scala 中函数和方法不完全一样, 很多时候可以混用, 因为编译器会把方法转换为函数 (方法执行
_
后变为函数, 例如add _
).函数的类型:
是输入类型和输出类型的一个简单组合, 由一个箭头从输入类型指向输出类型, 例如
def double(x: Int): Int = x + 2
的函数类型为Int=>Int
或(Int)=>Int
(只有一个参数时可以省略括号), 函数作为参数时会用到函数类型定义形参.Lambda 的占位符语法:
是 Lambda 的一种缩写形式, 讲命名参数替换为通配符
_
, 按顺序替换参数, 每个参数最多使用一次1
2
3
4// Lambda 的使用: 参数为函数
def foo(f: (Int, Int) => Int, a: Int, b: Int): Int = f(a, b) // 有 3 个参数: 第一个为函数, 第二三个为整数
foo(_ + _, 3, 4) // 直接写函数体, 更简洁, 缓存函数的动画计算部分使用这种方式会非常舒服
foo((a, b) => a + b, 3, 4) // 完整形式方法: 使用 def 定义:
1
def add(a: Int, b: Int): Int = a + b
函数使用
=>
定义, 方法使用=
Java 里 Lambda 使用
->
, JS 里 Lambda 使用=>
, Scala 里 Lambda 使用=>
高阶函数: 将其他函数作为参数或者返回值为函数的函数
使用表达式块调用高阶函数 (传名调用: The
=> Type
notation stands for call-by-name), 柯里化的时候调用也更舒服1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21def main(args: Array[String]): Unit = {
val result = timer {
util.Random.setSeed(System.currentTimeMillis)
for (i <- 1 to 10000) util.Random.nextDouble
util.Random.nextDouble
}
println(result)
}
// 注意:
// 参数不是 f: ()=> A, 这个是 Lambda
// 参数是 =>A, 表示传名调用
// What's the difference between => , ()=>, and Unit=>: https://stackoverflow.com/questions/4543228/whats-the-difference-between-and-unit
def timer[A](f: => A): A = {
val start = System.currentTimeMillis
val a = f // 函数执行
val end = System.currentTimeMillis
println(s"Executed in ${end - start}ms")
a
}偏应用函数 (Partial Applied Function, 又叫部分应用函数)
指一个函数有 n 个参数, 而我们为其提供少于 n 个参数, 那就得到了一个部分应用函数。
如果函数传递所有预期的参数, 则表示完全应用它 (完全参数应用函数), 如果只是传递几个参数并不是全部参数, 那么将返回部分应用的函数, 这样可以方便的绑定一些参数, 其余的参数可稍后填写补上
1
2
3
4def full(a: Int, b: Int): Int = a + b
val part1 = full(10, _: Int) // 简写
val part2 = (x: Int) => full(10, x) // 函数调用
val part3: Int => Int = full(10, _: Int) // 写上返回类型偏函数, 参考 https://www.jianshu.com/p/0a8a15dbb348, http://zhuanlan.51cto.com/art/201703/534053.htm
Scala 中的 Partial Function 是一个 Trait,其类型为 PartialFunction[A, B],其中接收一个类型为 A 的参数,返回一个类型为 B 的结果.
如果一组 case 语句没有涵盖所有的情况,那么这组 case 语句就可以被看做是一个偏函数, case 语句从本质上讲就是 PartialFunction 的子类。偏函数中最常见的组合方法为 orElse、andThen 与 compose, orElse 相当于一个或运算,如果通过它将多个偏函数组合起来,就相当于形成了多个 case 合成的模式匹配。倘若所有偏函数满足了输入值的所有分支,组合起来就形成一个函数了。
1
2
3
4
5
6
7
8
9
10
11
12// 求绝对值的运算
val positiveNumber: PartialFunction[Int, Int] = {
case x if x > 0 => x
}
val zero: PartialFunction[Int, Int] = {
case x if x == 0 => 0
}
val negativeNumber: PartialFunction[Int, Int] = {
case x if x < 0 => -x
}
def abs(x: Int): Int = (positiveNumber orElse zero orElse negativeNumber)(x)
1 | scala> val pf:PartialFunction[Int,String] = { |
艺术地说,Scala 中的 Partial Function 就是一个“残缺”的函数,就像一个严重偏科的学生,只对某些科目感兴趣,而对没有兴趣的内容弃若蔽履。Partial Function 做不到以“偏”概全,因而需要将多个偏函数组合,最终才能达到全面覆盖的目的。
利用 andThen 组合偏函数,设计本质接近 Pipe-and-Filter 模式,每个偏函数都可以理解为是一个 Filter。因为要将这些偏函数组合起来形成一个管道,这就要求被组合的偏函数其输入值与输出值必须支持可串接,即上一个偏函数的输出值会作为下一个偏函数的输入值。
泛型: 使用
[T]
定义泛型1
2
3
4
5
6
7
8def generic[A](a: A): Unit = {
println(a)
}
// 泛型函数调用
generic(1)
generic("String")
generic[String]("String")
数组
和 Java 一样, 数组 Array 的长度是不可变的:
1 | val arr: Array[Int] = new Array[Int](3) |
Array 几个重要的方法有 foreach, map, filter, flatten, flatMap, groupBy, reduce, fold, mkString:
foreach: 遍历数组的每一个元素
1
2
3
4
5arr1.foreach(println)
for (e <- arr1) {
println(e)
}map: 映射, 把一个元素映射为另外一个元素, 返回一个新的数组, 不影响原来的数组, 元素的类型可以改变
1
2
3
4
5// 把每一个元素乘以 10
val arr1 = Array(1, 2, 3, 4) // 初始化
val arr2 = arr1.map((e: Int) => e * 10)
val arr3 = arr1.map(e => e * 10)
val arr4 = arr1.map(_ * 10)filter: 传入一个 predication Lambda, 过滤留下符合条件的元素
1
2val arr1 = Array(1, 2, 3, 4, 5)
val arr2 = arr1.filter(_ > 3)flatten (扁平化, 将
列表的列表
转换为列表
): 数组的元素是数组, 使用 flatten 把元素给展开放在新数组里1
2
3val arr1 = Array(Array(1, 2, 3), Array(4, 5, 6))
var arr2 = arr1.flatten
println(arr2.toBuffer)flatMap: 先 map 再 flatten
1
2
3
4
5
6
7
8
9val arr1 = Array("Alice Bob John", "One Two Three")
val arr2 = arr1.flatMap(_.split(" ")) // lambda 执行后的结果是一个数组,然后执行 flatten (作用于列表的列表)
val arr3 = arr1.map(_.split(" ")).flatten // _.split(" ") 的结果是一个数组
println(arr2.toBuffer)
println(arr3.toBuffer)
ArrayBuffer(Alice, Bob, John, One, Two, Three)
ArrayBuffer(Alice, Bob, John, One, Two, Three)groupBy: 把数组中的元素分组, 返回一个 Map, key 为元素传入 Lambda 计算后的结果, value 为有相同 key 的元素的 List
1
2
3
4
5val arr = Array("Five Four Three Two Two", "One Two Three")
val map = arr.flatMap(_.split(" ")).groupBy(e => e) // 函数对每个元素计算的结果作为 key, 元素作为此 key 对应数组的元素
// 统计单词的数量, 使用 mapValues: 对 Map 的 values 进行映射, key 不变, 值为 key 对应 values 计算的结果
arr.flatMap(_.split(" ")).groupBy(e => e).mapValues(e => e.length) // 输出: Map(One -> 1, Five -> 1, Two -> 3, Four -> 1, Three -> 2)reduce: 讲数组折叠为单个元素
第一个参数为累积结果, 第二个参数为数组的元素
1
2
3
4
5val ns = Array(1, 2, 3, 4, 5)
ns.reduce((accumulation, e) => {
println(accumulation + ", " + e)
accumulation + e
})
不可变集合
List, Set, Map, 下标从 0 开始:
1 | val list = List(1, 2, 3, 4) // list(0), list(1), list.size, Nil |
list:
List()
创建一个空 List, 等于Nil
(List[Nothing] 的单例对象),val list2 = 2 :: list1
在 list1 头部插入 2 创建新的 ListList(1, 2, 3)
调用的是List.apply(1, 2, 3)
返回一个 List 对象, 只是一个语法糖:1
2
3
4
5
6
7
8
9object Hello {
def apply(): Unit = {
println("applied")
}
def main(args: Array[String]): Unit = {
Hello()
}
}
单元素集合
- Option
- Try
- Future
类
构造函数有 3 中: 默认构造函数, 主构造函数, 辅助构造函数, 伴生对象也是很重要的特性:
默认构造函数:
1
2
3
4class Teacher {
var name: String = _ // _ 会为根据类型进行初始化, 字符串为 null, 整数为 0
var age: Int = _ // 属性也可以用 private 修饰
}主构造函数: 主构造函数中 var | val 修饰的形参自动为类的成员变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 主构造函数中 var | val 修饰的形参自动为类的成员变量
class Teacher(var name: String, var age: Int) {
}
// 创建类的对象和访问成员变量
var teacher = new Teacher("Alice", 22)
println(teacher.name)
// 在主构造函数前加 private 则外界不能访问, 只能辅助构造函数访问, 形参也可以用 private 修饰
class Teacher private (var name: String, private var age: Int) {}
// 限制类的访问范围
private[this] class Teacher {} // 当前包下可访问辅助构造函数: 可以有多个辅助构造函数
1
2
3
4
5
6
7
8
9class Teacher(name: String, age: Int) {
var gender: Boolean = _
// def this 定义辅助构造函数, 必须先调用主构造函数或者其他辅助构造函数
def this(name: String, age: Int, gender: Boolean) = {
this(name, age)
this.gender = gender
}
}伴生对象: 类的同名对象, 可以用伴生对象的
apply()
函数创建对象, 例如List(1, 2, 3)
1
2
3
4
5
6
7
8
9
10
11
12class Teacher (var name: String, var age: Int) {
}
// 类 Teacher 的伴生对象
object Teacher {
def apply(): Teacher = {
new Teacher("Alice", 22)
}
}
println(Teacher().name) // 使用伴生对象调用 apply 创建 Teacher 的对象对象变量名后跟上
()
即调用apply()
函数, 例如 List 对象访问下标指定的元素list(3)
,apply()
方法实际是一个快捷方式, 可以使用小括号触发而不需要方法名:1
2
3
4
5
6
7
8
9
10
11
12// List 下标访问元素
val list = List(0, 1, 2, 3)
list(2)
// 类定义
class Multiplier(factor: Int) {
def apply(input: Int) = input * factor
}
val tripleMe = new Multiplier(3)
tripleMe.apply(10) // 返回 30
tripleMe(10) // 返回 30, 等于 tripleMe.apply(10)抽象类: 使用 extends 实现继承 (trait 也是使用 extends 进行实现)
1
2
3
4
5
6
7abstract class AbsClass {
def foo(): Unit
}
class AbsClassImpl extends AbsClass {
override def foo(): Unit = println("AbsClassImpl")
}case class: 可以使用 match 模式匹配, 默认实现了 Serializable 接口 (case object 也一样, 不需要封装数据的就用 case object)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16case class Bomb(name: String, sn: String) // 空的类体 {} 可要可不要
case object Manufacture
def matchTest(obj: Any): Unit = {
obj match {
case Bomb("2", "2") => println("2-2")
case Bomb(x, y) => println("Bomb")
case Manufacture => println("Manufacture")
}
}
def main(args: Array[String]): Unit = {
matchTest(Bomb("One", "ONe")) // case class 类创建对象可以不需要 new
matchTest(new Bomb("2", "2"))
matchTest(Manufacture)
}
接口 trait
Scala 中把 trait
叫做特质, 其实就是接口, 相当于 Java 中的 @FunctionalInterface
, 可以定义有实现和没有实现的方法, object 和 class 都能扩展 trait, 只有 extends, 没有 implements, 使用 override 避免重写错误:
1 | trait Flyable { |
创建对象的时候可以动态的使用 with
混入 trait:
1 | def main(args: Array[String]): Unit = { |
类定义的时候 extends traits:
1 | class Teacher (var name: String, var age: Int) extends Flyable with Foo { |
隐式类型转换
当编译器看到需要类型 X 却给了类型 Y, 它就在当前作用域查找是否定义了从类型 Y 到类型 X 的隐式定义, 如果有则转换类型 Y 为 X, 否则编译器报错:
implicit def File2MagicFile(file: File): MagicFile = new MagicFile(file)
val file: MagicFile = new File("data.txt")
- 这时 file 就自动转换为 MagicFile 的对象了, 同时具有 File 和 MagicFile 的行为
下面把 Double 隐式转换为 Int:
1 | implicit def double2Int(x: Double): Int = x.toInt // Double 转换为 Int |
隐式类型转换的代码可以放到一个 object 中 (不是 class 中), 然后在使用隐式转换的代码处导入这个 object 的文件, 这样可以统一的管理隐式转换:
1 | // Implicits.scala |
1 | // Hello.java |
- 隐式参数: 当我们在定义方法时,可以把最后一个参数列表标记为implicit,表示该组参数是隐式参数。一个方法只会有一个隐式参数列表,置于方法的最后一个参数列表。如果方法有多个隐式参数,只需一个implicit修饰即可。 当调用包含隐式参数的方法是,如果当前上下文中有合适的隐式值,则编译器会自动为改组参数填充合适的值。如果没有编译器会抛出异常。当然,标记为隐式参数的我们也可以手动为该参数添加默认值
- 隐式地转换类型: 使用隐含转换将变量转换成预期的类型是编译器最先使用 implicit 的地方。这个规则非常简单,当编译器看到类型X而却需要类型Y,它就在当前作用域查找是否定义了从类型X到类型Y的隐式定义
- 隐式调用函数: 隐式调用函数可以转换调用方法的对象,比如但编译器看到X .method,而类型 X 没有定义 method(包括基类)方法,那么编译器就查找作用域内定义的从 X 到其它对象的类型转换,比如 Y,而类型Y定义了 method 方法,编译器就首先使用隐含类型转换把 X 转换成 Y,然后调用 Y 的 method
泛型
1 | class Generic[T](vox: T) { |
枚举
Scala 中没有枚举, 但可以继承 Enumeration 实现
1 | object Colors extends Enumeration { |
Misc
Scala 中没有 static, object 中定义的内容都可以直接调用 (object 是一个单例对象, 不能 new).
为类型定义别名使用 type
(和 C 中的 typedef 一个作用): type M = collection.mutable.HashMap[Int, String]; val m: M = new M()
.
导入时给类型重命名:
1 | import java.util.{HashMap => JavaHashMap} |
排序用的 Ordered 相当于 Java 的 Comparable, Ordering 相当于 Java 的 Comparator.
根据 object 的 lazy 变量的表达式部分只执行一次的特点执行初始化工作:
1 | object Config { |
函数 apply()
, unapply()
, update()
- apply: 参数到对象, 例如
List(1, 2, 3)
创建对象 - unapply: 解构对象到变量, 按照主构造函数的参数顺序, match case 常用, 如
case Currency(amount, "USD") => println("USD: " + amount)
- update: 更新操作,
map(1) = "One"
等价于map.update(1, "One")
1 | object AppDemo { |
什么是尾递归
1 | 参考什么是尾递归 https://www.zhihu.com/question/20761771 |