基础
1.1 声明值和变量
在Scala中,鼓励使用val;
不需要给出值或变量的类型,这个信息可以从初始化表达式推断出来。在必要的时候,可以指定类型。
在Scala中,仅当同一行代码中存在多条语句时才需要用分号隔开。
1.2 常用类型
Scala 7中数值类型:Byte, Char, Short, Int, Long, Float和Double,以及Boolean类型。跟Java不同的是,这些类型是类。可以对数值执行方法,1.toString()
Scala不需要类型包装,类型之间的转换是Scala编译器的工作。简单的说,只需要了解上面7中数值类型,不需要了解更多StringOps或者RichInt等功能更强大的类。
Scala中,用方法而不是强制性类型转换,来做数值类型之间的转换。99.55.toInt => 99 "99.44".toDouble => 99.44
1.3 算术和操作符重载
中置表达法 x op y 相当于 x.op(y), 后置表示法 x op 相当于 x.op()
只有+ - ! ~能作为前置符使用
赋值语句 x op=y 相当于 x = x op y
对于数字,没有++, --操作
1.4 调用函数和方法
相比Java,Scala中使用数学函数(min, max, pow)更简单——不需要从某个类调用它的静态方法。
import scala.math._ or import math._
Scala没有静态方法,不过它有个类似的特性,叫做单里对象(singleton object)。通常,一个类对应有一个伴生对象(companion object),其方法更Java中的静态方法一样!
1.5 apply方法
在Scala中,我们用apply实现类似函数调动的语法,比如 “hello”(4) 是“hello”.apply(4)的简写。
此外,使用伴生对象的apply方法是Scala中构建对象的常用方法。例如 Array(1,3,4,5)返回一个数组,用的是Array伴生对象的apply方法。
控制结构和函数
2.1 条件表达式
Scala的if/else语法和Java,C++一样。不过,在Scala中if/else表达式有值!值是跟在if或者else之后的表达式的值。例如 val s = if (x > 0) 1 else -1
Scala每个表达式都有类型。if/else如何是混类型,则表达式的类型为两个分类型的公共超类型。
如果else缺失,则if语句可能没有值,Scala引入Unit类,写作(),可以把Unit当作 Java或C++中的void。严格来说,void没有值但是Unit有一个表示“无值”的值。
Scala没有switch语句,但是有比switch更强大的模式匹配机制。
2.3 块表达式和赋值
和Java, C++一样,块语句是一个包含于{}中的语句序列。Scala中,{}块中最后一个表达式是块的值
Scala中赋值语句没有值!——更严格的讲,他们的值是Unit类型。这点不同于Java和C++,因此 x = y = 1 是不正确的用法。
2.4 循环
Scala有与Java和C++相同的while和do循环。但是没有相同的for结构。相对的提供 for( i <- 表达式) 让变量 i 遍历右边表达式(scala集合)所有的值。
遍历数组或者字符串时,还可以通过下标遍历。需要使用从0~n-1的区间。这个时候可以使用until。 for(i <- 0 until s.length)
Scala中并没有提供break或continue语句退出循环。如果需要可以
- 使用Boolean型控制变量
- 使用嵌套函数——在函数中使用return
- 使用Breaks对象中的break方法,但是比较耗时
2.5 高级for循环和for推导式
生成器1: 可以以 变量 <- 表达式 的形势提供多个生成器,用分号隔开。例如: for(i <- 1 to 3; j<- 1 to 3) print ((10 * i) + j + " ")
生成器2: 每个生成器可以带一个“守卫”,以if开头的Boolean表达式。例如:for(i <- 1 to 3; j<- 1 to 3 if i != j) print ((10 * i) + j + " ")
生成器3: 可以使用任意多的定义,引入在循环中使用的变量。例如: for(i <- 1 to 3; from = 4-i; j <- from to 3) print(...)
推导式:如果for循环的循环体以yield开始,则该循环会构造出一个集合,每次迭代生成集合中的一个值。例如:for(i <- 1 to 10) yield i%3 //生成Vector(1,2,0,1,2,0,1,2,0,1)
生成器和推导式可以混合使用
2.6 函数
Scala中,方法对对象进行操作,函数不是。C++有函数,Java用静态方法模拟函数。
要定义函数,需要给出函数名称,参数和函数体。比如: def abs(x: Double) = if (x > 0) x else -x
如果函数不是递归,就不需要指定返回类型。Scala可以通过 = 右侧的表达式类型推断出返回类型
我们可以把return当作函数版的break语句,仅在需要的时候使用。
2.7 变长参数
def sum(args: Int*) 函数得到的是一个类型为Seq的参数。
不能将一个值的序列传入上述函数。结果的方法是追加: _*。例如 val s = sum(1 to 5: _*)
2.8 过程
Scala对于无返回值的函数有特殊的表示法。对于返回类型的是Unit或者没有前面的=,这样的函数被称作是过程(procedure)。
2.9 懒值
当val 被声明为 lazy,它的初始化将被推迟,直到我们首次对它取值。懒值对于开销较大的初始化语句很有用。
2.10 异常
Scala异常的工作机制和Java或C++一样。和Java一样,抛出的对象必须是java.lang.Throwable的子类。与Java不同,Scala不需要声明函数或方法可能会抛出某种异常。
throw表达式有特殊类型Nothing的值,在if/else表达式中比较有用
数组相关操作
3.1 定长和变长数组
若长度固定则使用Array。若长度可能有变化则使用ArrayBuffer(数组缓冲),类似Java中的ArrayList,C++中的vector
提供初始值时不要使用new(因为有apply)
3.2 遍历数组和数组缓冲
用()来访问元素
使用下标版: for(i <- 0 until arr.length)
不使用下标版:for(elem <- arr)
3.3 数组转换
转换数组不会修改原有数组,而是产生一个新的数组
for(...) yield 循环创建一个类型与原始集合相同的新集合。例如:val result = for (elem <- arr) yield 2*elem // result = Array(...)
除了for循环,还可以使用filter, map来完成数组转换。例如:a.filter(_ % 2 == 0).map(2 * _)
从数组缓冲中移除某个元素效率并不高。
3.4 与Java的互操作
由于Scala数组是Java数组(Java.util.List)实现的,我们可以在Java和Scala之间来回传递。在Scala代码中,可以引入 scala.collection.JavaConversions 里的隐式转换方法。
映射Mapping和元组
4.1 构造映射
不可变映射:val scores = Map("Alice" -> 10, "Bob" -> 3, "Cindy" -> 8)
可变映射:val scores = new scala.collection.mutable.HashMap[String, Int]
4.2 获取映射中的值
可以使用()来查找某键对应的值。例如 val bobscore = scores("Bob")。如果不存在该键,则抛出异常
更安全的写法:val bobscore = scores.getOrElse("Bob", 0)
最后使用Option:val bobScoreOption = scores.get("Bob")
4.3 更新映射中的值
scores("Bob") = 7
scores += ("Fred" -> 4)
scores -= "Alice"
虽然不可以直接更新一个不可变的映射,但仍可以通过修改得到新的映射。比如:
val newScores = scores + ("Alice" -> 10); //添加
scores = scores - "Alice" //删除
4.4 遍历映射
for((k, v) <- 映射)
for(v <- scores.values)
for((k, v) <- 映射) yield (v, k)
4.5 与Java的互操作
import scala.collection.JavaConversions._
例如 val scores: scala.collection.mutable.Map[String, Int] = new java.util.TreeMap[String, Int]
val props: scala.collection.mutable.Map[String, String] = System.getProperties()
4.6 元组
元组的值是通过将单个的值包含在圆括号中构成的。例如:val t = (1, 3.14, "Fred")是一个元组,类型为 Tuple3[Int, Double, java.lang.String]
可以通过 t._1(or t _1) t._2(or t _2) t._3(or t _3)访问元组的组元;或者使用模式匹配方式,例如:val (first, second, third) = t 或 val (first, second, _) = t
4.7 拉链操作
比如:keys.zip(values).toMap
类
Scala中类并不声明为public;属性,方法默认为public
5.1 setter和getter
类中的字段必须初始化。字段自动带有getter和setter方法。
- 如果字段是private,则getter和setter方法也是私有的。
- 如果字段是val,则只有getter方法被生成。
- 如果你不需要任何getter或setter,可以将字段声明为private[this]
5.2 定制getter/setter
5.3 Bean属性
Scala提供getter/setter方法,但是属性名字却不是getXXX/setXXX。为了生成Java规范的getter/setter。可以将Scala字段标注为@BeanProperty。
例如:@BeanProperty var name: String = _
将会生成四个方法: 1) name: String
2) name_=(newValue: String): Unit
3) getName(): String
4) setName(newValue: String): Unit
5.4 主构造器和辅助构造器
主构造器可以为 class Person(val name: String, val age: Int) 或者当类名之后没有参数时自动生成的无参构造器
辅助构造器同Java或C++的构造器十分相似。只是名字为this,且必须以先前定义的辅助构造函数或者主构造器调用开始。
针对主构造器参数生成的字段和方法
主构造器参数 | 生成的字段和方法 |
name: String | 对象私有字段。如果没有使用name, 则没有该字段 |
private val/var name: String | 私有字段,私有的getter/setter |
val/var name: String | 私有字段,公有的getter/setter |
@BeanProperty val/var name: String | 私有字段,公有Scala和java版的getter/setter |
5.5 嵌套类
Scala中嵌套类,可以看作是类的字段。
对象
6.1 单例对象
Scala没有静态方法或字段,用object这个语法结构可以达到相同的目的。
对象的构造器在该对象第一次被调用时调用。对象本质上拥有类所有特性——它甚至可以扩展其他类或者特质。只有一个例外:不能提供构造参数。
常用的场景:
- 作为存放工具函数或常量的地方
- 高效地共享单个不可变实例
- 需要用单个实例来协调某个服务时(参考单例模式)
6.2 伴生对象
在Java或C++中,通常既有实例方法又有静态方法的类。在Scala中,我们可以通过类和类同名的“伴生”对象来达到相同的目的。
类和伴生对象可以相互访问私有特性。他们必须存在于同一个源文件中。
6.3 扩展类或特质的对象
6.4 apply方法
通常我们会定义和使用对象的apply方法。例如: object(arg1, ..., argN) ,这样apply方法返回的时伴生类的对象
6.5 应用程序的对象
每个Scala程序都必须从一个对象的main方法开始,这个方法的类型为 Array[String] => Unit
除了每次都提供自己的main方法外,对象还得扩展App特质。例如:object hello extends App { ... }
6.6 枚举
不同于Java或C++,Scala并没有枚举类型。不过,标准库提供一个Enumeration助手类,可以产生枚举。
例如:object TrafficLightColor extends Enumeration {val RED, YELLOW, GREEN = Value}
这里定义了三个字段:RED, YELLOW, GREEN, 然后用Value调用将他们初始化。每次调用Value都返回内部类的新实例,该内部类也叫做Value。或者可以向Value方法传入ID、名称。如果不制定,则ID会将前一个枚举值+1,从0开始。缺省名称为字段名。
注意:枚举的类型是TrafficLightColor.Value,而不是TrafficLightColor。
对TrafficLightColor.values的调用输出所有枚举值的集合。枚举值的ID通过id方法返回,名称通过toString方法返回。
包和引入
7.1 重命名和隐藏方法
import java.util.{HashMap => JavaHashMap}
import scala.collection.mutable._
这样一来,JavaHashMap就是java.util.HashMap, 而HashMap对应scala.collection.mutable.HashMap
选取起 HashMap => _ 会将HashMap隐藏掉而不是重命名它。这仅当你需要引入其他成员时有用: import java.util.{HashMap => _, _}
继承
8.1 扩展类
Scala扩展类和Java一样——使用extends关键字
和Java一样,类声明为final,这样它就不能被扩展。单个方法或字段声明为final,以确保他们不能被重写。
8.3 重写方法
Scala中重写非抽象方法必须使用override修饰符。
Scala中调用超类方法和Java完全一样,使用super关键字。
8.3 类型检查和类型转换
要测试某个类是否属于某个给定的类,可以使用isInstanceOf方法,如果返回true可以使用asInstanceOf转换为子类的引用。如果时false,直接使用asInstanceOf,会返回null(当被转换引用为null时)或者直接抛出异常。
if(p.isInstanceOf[Employee]) { val s = p.asInstanceOf[Employee] }
如果要测试p指向一个Employee对象而不是其子类的话,可以用 if(p.getClass == classOf[Employee])
另一种更高效的方式是模式匹配。例如:p match { case s: Employee => ... case _ => ...}
8.4 抽象类
和Java一样,可以用abstract关键字来标记不能被实例化的类,通常这是因为它的某个或某几个方法没有被完整定义。
在子类中重写超类的方式时,不需要使用override关键字。
8.5 抽象字段
除了抽象方法外,类还可以有抽象字段(val or var)。抽象字段就是在抽闲类中没有初始化的字段。他们带有抽象的getter/setter方法
8.10 构造顺序和提前定义
8.11 Scala继承层次
AnyVal:与Java中基本类型相对应的类,以及Unit类型,都扩展自AnyVal
AnyRef:其他的类都是AnyRef的子类,AnyRef相当于Java中的Object类
AnyVal和AnyRef都扩展自Any类,而Any类是整个继承层次的跟节点。在继承层次上之外,还有两个特质:Nothing和Null类型。Nothing多用于泛型结构,比如:空列表Nil的类型是List[Nothing]。而Null类型唯一的实例就是null,我们可以将null赋值给任何引用(AnyRef类型),但不可以给AnyVal类型。
8.12 对象的相等行
在Scala中,AnyRef的eq方法检查两个引用是否指向同一个对象(地址比较?)。因此大多数情况下,我们需要重写equals方法和hashCode方法。
在重写equals方法时,定义 final override def equals(other: Any) = { ... }
文件和正则表达式
9.1 文件读写
读文件: val source = Source. fromFile("file.txt", "UTF-8")
读URL:val source = Source.fromURL("http://baidu.com", "UTF-8")
读二进制:Scala并没有提供读取二进制文件的方法。这里需要使用Java类库
9.2 进程控制
scala.sys.process包提供了用于与shell程序交互的工具。我们可以用Scala编写shell脚本。
例如:import sys.process._
"ls -al .." !
sys.process包含了一个从字符串到ProcessBuilder对象的隐式转换。!操作符执行的就是这个ProcessBuilder对象,返回的结构是执行程序的返回值:成功的话是0,否则显示错误的非0值。
如果使用 !!,而不是 !,输出结果会以字符串形式返回。
如果需要在不同的目录下运行程序,或者使用不同的环境变量,用Process对象的apply方法构造Process Builder,然后用! 执行。
9.3 正则表达式
构造Regex对象,用String类的r方法即可。例如:val numPattern = "[0-9]+".r
如果正则表达式包含反斜杠或引号的话,使用“原始”字符串的语法""" ... """。例如:val wsnumsPattern = """ \s+[0-9]+\s+""".r
findAllIn方法返回遍历所有匹配项的迭代器。
9.4 正则表达式组
类似Java的用法。例如:
val numItemPattern = "([0-9]+) ([a-z]+)".r
val numItemPattern(num, item) = "99 bottles"
如何想从多个匹配组中提取内容,可以使用for语句
for(numItemPattern(num, item) <- numItemPattern.findAllIn("99 bottles, 100 bottles")) { ...}
特质
10.1 当作接口使用的特质
特质中未被实现的方法默认是抽象的,不需要将方法声明为abstract,重写特质的抽象方法也不需要override关键字。
10.2 在特质中重写抽象方法
特质重新抽象方法时,调用了super.方法,则需要加上abstract override关键字
10.3 特质中的具体字段
特质中的具体字段会自动成为类自己的字段
10.4 特质中的抽象字段
特质中未被初始化的字段在具体子类中必须被重写,不需要写override
10.5 特质构造顺序
和类一样,特质也有构造器,有字段的初始化和其他特质体中的语句构成。
顺序:超类》父特质》特质》子类
特质从左到右被构造;每个特质中,父特质先被构造
构造器的顺序是类的线性化的反向,线性化给出了在特质中super被解析的顺序。子类》子特质》父特质》父类
10.6 初始化特质中的字段
缺少构造器参数是特质与类之间的唯一技术差别。特质不能有构造器参数。每个特质都有一个无参数的构造器。
未被初始化的字段,在特质中一个小心使用。可以通过定义或者lazy初始化避开陷阱。
10.7 扩展类的特质
特质可以扩展类,这个类会自动成为所有混入该特质的超类。
10.8 自身类型
this: Type => 表明该特质只能混入制定类型的子类
操作符
高级函数
12.1 作为值的函数
import scala.math._
val fun = ceil _
这里将ceil方法转换成了一个函数。Scala无法直接操纵方法,但是可以直接操作函数。
12.2 匿名函数
(x: Double) => 3*x
12.3 带函数参数的函数
def valueAtOneQuarter(f: (Double) => Double) = f(0.25)
valueAtOneQuarter的类型为:((Double) => Double) => Double。由于valueAtOneQuarter是一个接受函数参数的函数,因此也被称为高阶函数(higher-order function)
12.4 参数(类型)推断
当你将匿名函数参数传递给另一个函数时,Scala会尽可能帮助你推断出类型信息。
如果匿名函数只有一个参数,可以省略()。例如:x => 3*x
如果参数在=>右侧只出现一次,可以用 _ 替换掉。例如:valueAtOneQuarter(3*_)
用 _ 的前提是参数类型已知。
12.5 柯里化
Methods may define multiple parameter lists. When a method is called with a fewer number of parameter lists, then this will yield a function taking the missing parameter lists as its arguments.
柯里化currying,指的是将原来接收多个参数的函数变成新的接受一个参数的函数的过程。
柯里化行为本质上也是一个高阶函数:接受现有的函数,返回新函数。
这个高阶函数就是 curried
:curried
方法存在于 Function2
、 Function3
这样的多参数函数类型里。