Scala 学习笔记

Contents:

第一章 基础

常用类型

scala中常用类型如下:

  • Byte
  • Char
  • Short
  • Int
  • Long
  • Float
  • Double
  • Booean

这些类型都是类,所以在scala中不需要包装类型,在基本类型和包装类型之间的转换工作是scala编译器的事。

除此之外,还有 Null 类型,指代 null 或者空引用。

Nothing 是所有其它类型的子类型,包括空值。

Any 是其它类型的父类型, AnyRef 是其它引用类型的父类型。

在scala中,我们使用方法而不是强制类型转换,来做数值类型之间的转换。例如:

99.44.toInt         // 99
99.toChar           // 'c'

和Java一样, toString 将任意对象转换成字符串,要将包含了数字的字符串转换成数字,使用 toInt 或者 toDouble

算术和操作符重载

和Java相比,Scala并没有提供 ++-- 操作符,我们需要使用 +=1 或者 -=1

对于常规的 BigIntBigDecimal 对象,我们可以使用常规的方法使用那些数学操作符:

val x:BigInt = 12121212
x*x*x

在java中,我们需要使用 x.multiply(x).multiply(x)

调用函数和方法

相比Java,在scala中使用数学函数更简单,我们不需要从某个类的调用它的静态方法。

import scala.math._
sqrt(2)
pow(2,4)

在使用以scala开头的包时,我们可以省略scala的前缀。例如 import math._ 等价于 import scala.math._

Scala中没有静态方法,但是提供了单例对象。

不带参数的方法通常不使用圆括号,一般来讲,没有参数并不改变当前对象的方法都不带圆括号。

scala中允许使用数字 * 字符串,从而实现复制字符串的功能。

apply方法

在scala中,我们通常使用类似函数调用的语法。例如:

"Hello"(4) // o

你可以将这种用法当作 () 操作符的重载形式,它背后原理是实现一个名为 apply 的方法。所以 "Hello"(4) 相当于以下代码:

"Hello".apply(4)

第二章 控制结构和函数

条件表达式

scala的 if/esle 语法结构与Java一样,但是在scala中 if/else 表达式有值,这个值就是跟在 ifelse 之后表达式的值。

我们可以将 if/else 表达式的值赋予给变量:

val s = if (x>0) 1 else -1

它等价于

if (x>0) s = 1 else s = -1

不过,第一种写法更好,因为它可以用来初始化一个 val , 而第二种写法中, s 必须是 var

在Scala中,每个表达式都有一个类型。

如果是混合类型,则类型为 Any

如果 else 部分缺失,例如:

if (x>0) 1

等价于

if (x>0) else ()

Scala没有 switch 语句,但是它有一个更强大的模式匹配机制。

语句终止

在scala中,分号绝大多数情况下都不是必须的。不过如果你想在单行中写下多个语句,则需要将它们以分号隔开。

块表达式和赋值

在Scala中, {} 包含一系列表达式,块中最后一个表达式的值就是块的值。

在Scala中赋值语句是没有值的,所以别把它们串接在一起。

x = y = 1 //别这样做

输入和输出

使用 print 或者 println 打印一个值。

使用 printf 格式化输出。

使用 readLine 从控制台读取一行输入,如果是读取数字, Boolean 或者字符串,可以使用 readInt , readDouble , readByte , readShort , readLong, readFloat, readBoolean``或者 ``readChar。与其他方法不同,readLine 带一个参数作为提示字符串。

循环

scala支持 while 循环和 for 循环, while 循环与Java的 while 一样, for 循环语法如下:

for( i <- 表达式)

遍历字符串和数组时,你通常需要使用 0n-1 的区间,这个时候可以使用 until 方法而不是 to 方法。 until 方法返回一个并不包含上限的区间。

高级for循环和for推导式

可以使用变量 <- 表达式的形式提供多个生成器,用分号隔开。例如:

for(i <-1 to 3, j <- 1 to 3) print ((10*i+j)+ " ")

每个生成器还可以带过滤条件,以 if 开头的 Boolean 表达式。

for(i <-1 to 3, j <- 1 to 3 if i != j) print ((10*i+j)+ " ")

还可以使用任意多的定义,引入可以在循环中使用的变量:

for( i <- 1 to 3; from = 4-i; j <- from to 3)  print ((10*i+j)+ " ")

如果 for 循环的循环体以 yield 开始,则该循环会构造出一个集合,每次迭代出集合中的一个值:

for( i <- 1 to 10) yield i % 3

这类循环叫做 for 推导式。

函数

要定义函数,需要给出函数的名称、参数和函数体:

def abs(x:Double) = if (x>0) x else -x

必须给出所有参数的类型,不过,只要函数不是递归的,就不需要指定返回类型。Scala编译器可以通过 = 右侧的表达式推断出返回类型。

如果函数体需要多个表达式完成,可以使用代码块,块中最后一个表达式的值就是函数的返回值。

对于递归函数,必须指定返回类型。

默认参数和带名参数

scala中可以给函数提供默认参数:

def func(num:Int = 2) num += 3

还可以在提供参数值的时候指定参数名。带名参数不需要跟参数列表的顺序完全一致。

变长参数

scala中还支持接收可变长度参数列表:

def sum(args: Int*){
var result = 0
for(arg <- args)
        result += arg
result
}

函数得到的是一个类型为 Seq 的参数。

如果你已经有一个值的序列,则不能直接将它传进上述函数。例如:

val s = sum(1 to 5) //错误

如果 sum 函数被调用时传入的是单个参数,那么该参数必须是单个整数,而不是一个整数区间。解决这个问题的办法是告诉编译器你希望这个参数被当作参数序列来处理,追加 :_* 。例如:

val s = sum(1 to 5: _*)

过程

scala中不返回值的函数有特殊的表示法,如果函数体包含在花括号当中,但没有前面的 = 号,那么返回类型就是 Unit 。这样的函数称之为过程。

由于过程不返回值,所以我们省略 = 号。

懒值

val 被声明为 lazy 时,它的初始化将被推迟,直到我们首次对它赋值。

lazy val words = scala.io.Source.fromFile("a.txt").mkString

如果程序从不访问 a.txt ,那么它就不会被打开。 懒值对于初始化开销较大的初始化语句而言十分有用。

异常

scala异常工作机制与Java一样,但是scala没有受检异常。

throw 有特殊的类型值 Nothing ,这在 if/else 语句中特别有用,如果一个分支的类型是 Nothing ,那么 if/else 表达式的类型就是另一个分支的类型。

捕获异常的语法采用模式匹配的语法,更通用的异常应该排在更具体的异常后面。

如果不需要使用捕获的异常名,可以使用 _ 代替变量名。

第三章 字符串

在Scala中,字符串也是不可变对象。

创建字符串

var greeting = "Hello, world"

// or

var greeting:String = "Hello, world"

如果需要使用可变字符串,可以使用 StringBuilder 类。

字符串长度

使用 length() 获取字符串长度。

object Demo {
def main(args: Array[String]) {
        var palindrome = "Dot saw I was Tod";
        var len = palindrome.length();
        println( "String Length is : " + len );
}
}

拼接字符串

string1.concat(string2);

// or

string1 + string2

格式化字符串

可以使用 printf 或者 format 来格式化字符串。 String 类有一个等价类的方法, format() ,它返回一个 String 对象,而不是一个 PrintStream 对象。

scala > "%.2f".format(1212)
1212.00

字符串插值

字符串插值允许用户将变量的引用直接嵌入到处理字符串字面量中。例如:

val name = "James"
println(s"Hello, $name")    //Hello, James

scala提供了三种字符串插值方法: sf ``  ``raw

插值器s

在任何字符串之前添加 s ,则该字符串允许直接包含变量。

字符串插值器也可以包含任意表达式:

println(s" 1+1 = ${1+1}")

格式化插值器f

在任何字符串字面量前追加 f ,就可以创造一个简单的格式化字符串,类似于其他语言中的 printf 。 当使用插值器f时,所有变量的引用应该跟随 printf 风格的格式字符串,如同 %d 。 例如:

val height = 1.9d
val name = "James"
println(f"$name%s is $height%2.2f meters tail")  // James is 1.90 meters tall

插值器 f 是类型安全的。如果你试图传递一个只能工作于整数的格式化字符串,却又传了一个浮点数,编译器会发出一个错误。

插值器raw

插值器 raw 和插值器 s 相似,不同的是它不对字符串字面量执行转义。

scala> s"a\nb"
res0: String =
a
b

插值器 s 将字符 \n 替换成了回车符。而插值器 raw 不会这么做。

scala> raw"a\nb"
res1: String = a\nb

当你想要避免有表达式(例如 \n 变成回车)时,插值器 raw 是很有用的。

第四章 数组

定长数组

如果需要一个长度不变的数组,可以使用scala中的 Array

var z:Array[String] = new Array[String](3)
or
var z = new Array[String](3)

变长数组

变长数组使用 ArrayBuffer ,在数组缓存的尾端添加或者移除元素是一个高效的操作。

使用 toArray 将数组缓存转为数组,使用 toBuffer 将数组转成缓存。

遍历数组和数组缓存

使用 for 循环遍历数组或者缓存:

for( i <- 0 until a.length)
println(i)

如果想每两个元素一跳,可以这样遍历:

0 until (a.length,2)

如果想从数组的尾端开始,遍历写法为:

(0 until a.length).reverse

数组转换

从一个数组出发,以某种方式对它进行转换,这些转换操作不会修改原数组,而是产生一个新的数组。

val a = Array(1,2,3,4)
val result = for(elem <- a) yield 2*elem

结果返回一个类型与原始集合相同的新集合。

我们也可以给转换增加过滤条件:

for(elem <- a if elem % 2 == 0) yield 2 * elem

注意原始集合并没有受到影响。

另一种做法是:

a.filter(_ % 2 == 0).map(2 * _)

甚至:

a.filter{ _ % 2 == 0 } map {2 * _}

常用算法

求和

Array(1,2,3).sum

最小值和最大值

Array(1,2,3).min
Array(1,2,3).max

排序

Array(5,2,1,4).sortWith(_ < _)

显示数组内容

a.mkString
a.mkString(" and ")
a.mkString("<",",",">")

多维数组

import Array._

object Demo {

        def main(args: Array[String]) {
                var myMatrix = ofDim[Int](3,3)

                // build a matrix
                for (i <- 0 to 2) {
                        for ( j <- 0 to 2) {
                                myMatrix(i)(j) = j;
                        }
                }

                // Print two dimensional array
                for (i <- 0 to 2) {
                        for ( j <- 0 to 2) {
                                print(" " + myMatrix(i)(j));
                        }
                        println();
                }
        }
}

第五章 映射、选项以及元组

构造映射

不可变映射

val score = Map("Alice"->80)

可变映射

val score = scala.collection.mutable.Map("Alice"-90)

在scala中,映射是一个对偶,对偶是两个值构成的组,这两个值不一定是同一类型。

使用 -> 来创建对偶。

获取映射的值

在scala中使用 () 表示法来查找某个键对应的值。

val bobscore = scores("Bob")

如果映射并不包含请求中使用的键,则会抛出异常。

要检查映射中是否有某个指定的键,可以使用 contains() 方法。

除此之外,我们还可以使用 getOrElse() 方法。

更新映射中的值

在可变映射中,可以更新映射值,做法是在 = 的左侧使用 ()

scores("Bob") = 10

也可以使用 += 来添加多个关系

scores +=  ("Kate" -> 80)

使用 -= 移除某个键和对应的值。

迭代映射

使用 for 循环遍历映射中所有的键:

for( (k, v) <- map)

访问键或者值

scores.keySet
scores.values

反转映射

for( (k, v) <- map) yield (v, k)

已排序映射

val scores = scala.collections.immutable.SortedMap("A"->1, "B"->2)

如果需要按插入顺序访问所有键的话,使用 LinkedHashMap

选项

Option 是一个表示有可能包含值的容器。

Option 基本的接口是这样的:

trait Option[T] {
  def isDefined: Boolean
  def get: T
  def getOrElse(t: T): T
}

Option 本身是泛型的,并且有两个子类: Some[T]None

Map.get 使用 Option 作为其返回值,表示这个方法也许不会返回你请求的值。

模式匹配能自然地配合 Option 使用。

val result = res1 match {
  case Some(n) => n * 2
  case None => 0
}

元组

元组是不同类型值的集合。

val t = (1, "a", 3.14)

和数组或字符串中的位置不同,元组从1开始而不是0。

在创建两个元素的元组时,可以使用特殊语法: ->:

scala> 1 -> 2
res0: (Int, Int) = (1,2)

通常,使用模式匹配来获取元组的组员。

val (first , second, third ) = t

如果并不是所有的部件都需要,则可以在不需要的位置使用 _

val (first, second, _) =  t

常用集合操作

Map

map 对列表中的每个元素应用一个函数,返回应用后的元素所组成的列表。

scala> numbers.map((i: Int) => i * 2)
res0: List[Int] = List(2, 4, 6, 8)

或者传入一个函数:

scala> def timesTwo(i: Int): Int = i * 2
timesTwo: (i: Int)Int

scala> numbers.map(timesTwo _)
res0: List[Int] = List(2, 4, 6, 8)

foreach

foreach 很像 map ,但没有返回值。 foreach 仅用于有副作用的函数。

scala> numbers.foreach((i: Int) => i * 2)

该函数返回值为Unit类型。

filter

filter 移除任何对传入函数计算结果为 false 的元素。返回一个布尔值的函数通常被称为谓词函数[或判定函数]。

scala> numbers.filter((i: Int) => i % 2 == 0)
res0: List[Int] = List(2, 4)

scala> def isEven(i: Int): Boolean = i % 2 == 0
isEven: (i: Int)Boolean

scala> numbers.filter(isEven _)
res2: List[Int] = List(2, 4)

zip

zip 将两个列表的内容聚合到一个对偶列表中。

scala> List(1, 2, 3).zip(List("a", "b", "c"))
res0: List[(Int, String)] = List((1,a), (2,b), (3,c))

partition

partition 将使用给定的谓词函数分割列表。

scala> val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
scala> numbers.partition(_ % 2 == 0)
res0: (List[Int], List[Int]) = (List(2, 4, 6, 8, 10),List(1, 3, 5, 7, 9))

find

find 返回集合中第一个匹配谓词函数的元素。

scala> numbers.find((i: Int) => i > 5)
res0: Option[Int] = Some(6)

drop & dropWhile

drop 将删除前i个元素

scala> numbers.drop(5)
res0: List[Int] = List(6, 7, 8, 9, 10)

dropWhile 将删除元素直到找到第一个匹配谓词函数的元素。例如,如果我们在numbers列表上使用 dropWhile 奇数的函数, 1将被丢弃(但3不会被丢弃,因为他被2“保护”了)。

scala> numbers.dropWhile(_ % 2 != 0)
res0: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10)

foldLeft & foldRight

scala> numbers.foldLeft(0)((m: Int, n: Int) => m + n)
res0: Int = 55

0为初始值(记住numbers是 List[Int] 类型),m作为一个累加器。

foldRightfoldLeft 一样,只是运行过程相反。

flatten

flatten 将嵌套结构扁平化为一个层次的集合。

scala> List(List(1, 2), List(3, 4)).flatten
res0: List[Int] = List(1, 2, 3, 4)

flatMap

flatMap 是一种常用的组合子,结合映射 [mapping] 和扁平化 [flattening]flatMap 需要一个处理嵌套列表的函数,然后将结果串连起来。

scala> val nestedNumbers = List(List(1, 2), List(3, 4))
nestedNumbers: List[List[Int]] = List(List(1, 2), List(3, 4))

scala> nestedNumbers.flatMap(x => x.map(_ * 2))
res0: List[Int] = List(2, 4, 6, 8)

可以把它看做是“先映射后扁平化”的快捷操作。

第六章 类

简单类和无参方法

Scala类最简单的形式和Java相似:

class Counter {
    private var value = 0 //必须初始化
    def increment() { value += 1}   //方法默认是公有的
    def current () = value

在scala中,类并不声明为 public ,scala 源文件可以包含多个类,所有这些类都具有公有可见性。

调用无参方法时,可以使用圆括号,也可以不使用。一般的,对于改值器方法使用 () ,对于取值器方法则去掉 ()

也可以使用不带 () 的方式声明 current 来强制调用的时候不使用 ()

带有getter和setter的属性

Scala对每一个字段都提供了 gettersetter 方法。这里定义一个公有字段:

class Person {
    var age = 0
}

Scala生成面向JVM的类,其中有一个私有的 age 字段以及相应的 gettersetter 方法。这两个方法是公有的,因为我们没有将 age 声明为 private 。对于私有字段而言,gettersetter 也是私有的。

在scala中,gettersetter 分别叫做 ageage_=。 例如:

println(fred.age)
fred.age = 20

在任何时候你都可以自己重新定义 gettersetter 方法。

如果字段是 val ,则只有 getter 方法被生成。如果你不需要任何 gettersetter ,可以将字段声明为 private[this]

只带getter的属性

是用 val 字段,scala会生成一个 final 字段和一个 getter 方法。

对象私有字段

在scala中,方法可以访问该类的所有对象的私有字段。scala允许我们定义更加严格的访问限制,通过 private[this] 这个修饰符实现。

private[this] val value = 0 //类似某个对象.value这样的访问将不被允许

这样的访问有时候称之为对象私有的。

对于类私有的字段,scala会生成私有的 getter``和 ``setter 方法,但对于对象私有的字段,scala根本不会生成 gettersetter 方法。

scala允许将访问权赋予给指定的类,private[类名] 修饰符可以定义仅有指定类的方法可以访问给定的字段。这里的类必须是当前定义的类,或者是包含该类的外部类。

辅助构造器

和Java一样,Scala也可以有任意多的构造器,包括主构造器与辅助构造器。

辅助构造器与Java的构造器类似,只有两处不同:

  • 辅助构造器的名称为 this
  • 每一个辅助构造器都必须以一个对先前已定义的的其他辅助构造器或主构造器的调用开始。

下面的类中包含两个辅助构造器:

class Person{
    private var name = ""
    private var age = 0

    def this(name:String) {
        this()
        this.name = name
    }

    def this(name:String, age:Int){
        this(name)
        this.age = age
    }
}

一个类如果没有显示定义主构造器则自动拥有一个无参的主构造器。

现在,我们可以使用三种方式构建对象:

val p1 = Person
val p2 = Person("Jim")
val p3 = Person("Jim",12)

主构造器

在scala中,每个类都有主构造器,主构造器并不以 this 方法定义,而与类定义交织在一起。

  1. 主构造器的参数直接放置在类名之后。
class Person(val name:String, val age:Int){
    ...
}

主构造器的参数被编译成字段,其值被初始化成构造时传入的参数。

  1. 主构造器会执行类定义中的所有字段。例如:
class Person(val name:String, val age:Int){
    println("Just constructed another person")
    ...
}

println 语句是主构造器的一部分,每当有对象被构造出来的时候,上述代码就会被执行。当你需要再构造过程中配置某个字段的时候这个特性特别有用。

如果类名之后没有参数,则该类具备一个无参主构造器,这样的一个构造器仅仅简单的执行类体中的所有语句而已。

构造函数也可以是普通的方法参数,不带 valvar。这样的参数如何处理取决于它在类中如何被使用。

  • 如果不带 val 或者 var 参数至少被一个方法所使用,它将被升格为字段。例如:
class Person(name:String, age:Int){
    def description = name + " is " + age + " years old"
}

上述代码声明并初始化了不可变字段的 nameage。而这两个字段都是对象私有化的。类似这样的字段等同于 private[this] val 字段的效果。

  • 否则,该参数将不被保存为字段,它仅仅是一个可以被主构造器中的代码访问的普通参数表。

如果想让主构造器变成私有的,可以像这样放置 private 关键字。

class Person private (val id:Int){
    ...
}

这样一来,用户就必须通过辅助构造器来构造Person对象了。

嵌套类

在scala中,你几乎可以在任何语法结构中内嵌任何语法结构,你可以在函数中定义函数,也可以在类中定义类。

第七章 对象

单例对象

scala中没有静态方法或静态字段,不过可以使用 object 这个语法结构来达到同样的目的。对象定义了某个类的单个实例,包含了我们想要的特性。

例如:

object Accounts {
    private var lastNumber = 0
    def newUniqueNumber() = {lastNumber += 1; lastNumber }
}

对象的构造器在第一次使用时被调用,如果一个对象从未被使用,那么其构造器也不会被执行。

对象本质上可以拥有类的所有特性,它甚至可以扩展其他类和特质,只有一个例外,不能提供构造参数。

对于任何你在Java中会使用单例对象的地方,在Scala中都可以用对象实现:

  • 作为存放工具函数和常量的地方
  • 高效的共享单个不可变实例
  • 需要用单个实例来协调某个服务时。

伴生对象

在Java中,通常会用到既有实例方法又有静态方法的类,在类中,可以通过类和与类同名的伴生对象来实现。例如:

class Accounts {
    val id = Accounts.newUniqueNumber()
    private var balance = 0.0

    def despoit(amount:Double){balance += amount}
        ...
}

object Accounts {
    private var lastNumber = 0

    def newUniqueNumber() = {
        lastNumber += 1; lastNumber
    }
}

类和它的伴生对象可以互相访问私有特性,它们必须存在于同一个源文件中。

类的伴生对象可以被访问,但是并不在作用域中。

扩展类或特质的对象

一个对象可以扩展类以及一个或者多个特质,其结果是一个扩展了指定类以及特质的类的对象,同时拥有在对象定义中给出的所有特性。

一个有用的场景是给出可被共享的缺省对象。

apply方法

我们通常会定义和使用对象的方法,当遇到如下形式的表达式时, apply 方法就会被调用:

Object(arg1, ..., argN)

通常这样一个 apply 方法返回的是伴生类的对象。

应用程序对象

每个scala程序都必须从一个对象的 main 方法开始,这个方法的类型为 Array[String]=>Unit:

object Hello {

    def main(args:Array[String]){
        println("hello,world")
    }
}

除了每次都提供自己的 main 方法之外,我们还可以扩展 App 特质,然后将程序代码放到构造器方法体中:

object Hello extends APP {

    println("hello,world")

}

如果需要命令行参数,可以通过 args 属性获取。

枚举

和Java不同,scala并没有枚举类型,不过标准类库提供了一个 Enumeration 助手类,可以用于产生枚举。

定义一个扩展 Enumeration 类的对象并以 Value 方法调用初始化枚举中的所有可选值。

object TrafficLightColor extends Enumeration {

    val Red, Yellow, Green = Value

}

这里定义了三个字段: Red , Yeelow , Green 。然后调用 Value 方法进行初始化:

val Red = Value
val Yellow  = Value
val Green = Green

每次调用 Value 方法都返回内部类的新实例,该内部类也叫 Value 。 或者,也可以向 Value 方法传递ID、名称,或两个参数都传。

val Red = Value(0, "Stop")
val Yellow = Value(10)
val Green = Value("Go")

如果不指定,则ID将在前一个枚举值的基础上加一, 从零开始,缺省名称为字段名。

定义完成之后,就可以使用 TrafficLightColor.Red、TrafficLightCOlor.Yellow 等来引用枚举值了。

枚举值的ID可以通过id方法返回,名称通过 toString 方法返回。

第八章 包和引入

scala的包目的和Java中的包一样:管理大型程序中的名称。

要增加条目到包中,可以将其包含在包语句中,例如:

package com{
    package horstman {
        package patient{
            class Employee{
            ...
            }
        }
    }
}

这样一来,类名 Employee 就可以在任意位置以 com.horstman.patient.Employee 访问了。

与对象和类的定义不同,同一个包可以定义在多个文件中。

源文件的目录和包之间并没有强制的关联关系,也就是说可以在同一个文件中定义多个包。

作用域规则

在scala中,包的作用域比Java更加前后一致,scala的包和其他作用域一样支持嵌套。可以访问上层作用域中的名称。

在Java中包名是绝对的,从包层级的最顶端开始,但是在scala中,包名是相对的,就像内部类的名称一样。

串联式包语句

包语句可以包含一个串,或者说路径区段:

package com.hosrtman.patient{
    package people{
        class Person{
            ...
        }
    }
}

这条语句限制了可见的成员。

文件顶部标识法

除了使用嵌套标记法之外,还可以在文件顶部使用 package 语句,不带花括号。

package com.hosrtman.people.patient
package people

包对象

包可以包含类,对象和特质,但是不能包含函数和变量的定义。

每个包可以有一个包对象,你需要在父包中定义它,且名称和子包一样。 例如:

package com.horstman.patient

package object people{
    val defaultName = "John"
}

package people{
    class Person{
        val name = defaultName  //从包对象中拿到常量
        ...
    }
}

包可见性

在Java中,没有被声明为 publicprivateprotected 的类成员在包含该类的包中可见。在scala中,可以通过修饰符达到同样的目的。

引入

引入语句可以让你使用更短的名字而不是原来较长的名字。 写法如下:

import java.awt.Color

引入包中全部成员:

import java.awt._

任何地方都可以声明引入

在scala中, import 语句可以出现在任意地方,并不仅限于文件顶部, import 语句的效果一直延伸到包含该语句的块末尾。

重命名和隐藏

如果你想引入包中的几个成员,可以像这样使用选取器:

import java.awt.{Color, Font}

选取器还允许你重命名选到的成员:

import java.util.{HashMap => JavaHashMap}

选取器 HashMap => _ 将隐藏某个成员而不是重命名它。

隐式引入

每个scala程序都隐式的以如下代码开始:

import java.lang._
import scala._
import Predef._

由于scala包默认导入,对于那些以scala开头的包,你完全不需要写全这个前缀。

第九章 继承

扩展类

scala扩展类的方式和Java一样,使用 extends 关键字:

class Employee extends Person{
    val salary = 0.0
...
}

和Java一样,你可以将类声明为 final ,这样它就不能被扩展。你可以将单个方法或者字段声明为 final , 以确保它们不能够被重写。

重写方法

在scala中重写一个非抽象方法必须使用 override 修饰符。

public class Person{
...
override def toString = getClass.getName + "[name=" + name + "]"
}

override 修饰符可以用在多个情况下给出有用的错误提示。包括:

  • 当你拼错要重写的方法名
  • 当你不小心在新方法中使用错误的参数类型。
  • 当你在超类中引入新的方法,而这个新的方法与子类的方法相抵触。

在scala中调用超类的方法和Java一样,使用 super 关键字。

类型转换和检查

要检查某个对象是否属于某个特定的类,可以用 isInstanceOf 方法。如果测试成功,你就可以使用 asInstanceOf 方法将引用转换为子类的引用。

if (p.isInstanceOf[Employee]){
val s = p.asInstanceOf[Employee]
}

如果 p 指向的是 Employee 类及其子类,则 p.isInstanceOf[Employee] 就会成功;

如果 pnull , 则 p.isInstanceOf[Employee] 将返回 false ,且 p.asInstanceOf[Employee] 将返回 null

如果 p 不是一个 Employee ,则 p.asInstanceOf[Employee] 将抛出异常。

如果你想要测试 p 指向的是一个 Employee 对象但又不是其子类的话,可以使用:

if (p.getClass == classOf[Employee])

不过与类型检查和转化相比,模式匹配通常是更好的选择。

p match{
case s:Employee => ...
case _ =>...
}

受保护字段

如果字段或方法被声明为 protected ,则这样的成员可以被任何子类访问,但不能从其他位置看到。与Java不同的是,protected 的成员对于类所属的包而言,是不可见的。

超类的构造

辅助构造器永远不能够直接调用超类的构造器,子类的辅助构造器最终都会调用主构造器,只有主构造器可以调用超类的构造器。

scala类可以扩展Java类,这种情况下,它的主构造器必须调用Java超类的某一个构造方法。

重写字段

注意以下限制:

  • def 只能重写另一个 def
  • val 只能重写另一个 val 或不带参数的 def
  • var 只能重写另一个抽象的 var

匿名子类

和Java一样,你可以通过包含带有定义或者重写代码块的方式创建一个匿名子类。

val align = new Person("Fred"){
def greeting = "hello"
}

抽象类

和Java一样,可以使用 abstract 关键字定义一个不能被实例化的类。

abstract class Person(val name:String){
def id:Int
}

但是在scala中,不像java,你不需要对抽象方法使用 abstract 关键字,你只是省去方法体。

在子类中重写父类的抽象方法时,你不需要使用 override 关键字。

抽象字段

除了抽象方法之外,类还可以拥有抽象字段,抽象字段就是一个没有初始化值的字段。

具体的子类必须提供具体的字段。和方法一样,在子类中重写超类的抽象字段时,不需要 override 关键字。

scala继承层级

所有其他类都是 AnyRef 的子类, AnyRef 相当于Java中的 Object 类。

AnyValAnyRef 都扩展自 Any 类,而 Any 类是整个继承层级的根节点。

Null 类型的唯一实例就是 null 值,你可以额将 null 值赋值给任何引用,但不能赋值给值类型的应用。

Nothing 类型没有实例,它对于泛型结构时常有用。

对象相等性

在scala中, AnyRefeq 方法检查两个引用是否指向同一个对象。 AnyRefequals 方法调用 eq

第十章 特质

特质

scala中没有多重继承,scala提供特质而非接口,特质可以同时拥有具体方法和抽象方法,而类可以实现多个特质。

trait Equal {
        def isEqual(x: Any): Boolean
        def isNotEqual(x: Any): Boolean = !isEqual(x)
}

在重写特质的抽象方法时不需要使用 override 关键字。

如果你需要不止一个特质,可以用 with 关键字来添加额外的特质。

所有Java接口都可以作为scala特质来使用。

带有具体实现的特质

在scala中,特质的方法不一定需要是抽象的。

让特质拥有具体行为有一个弊端,当特质改变的时候,所有混入该特质的类都必须重新编译。

带有特质的对象

在构造单个对象时,可以给它添加特质。

val act = new SavingAccount with ConsoleLogger

在特质中重写抽象方法

首先定义特质 Logger

trait Logger {
        def log(msg:String)
}

接下来定义特质 TimestampLogger 并继承重写 log 方法:

trait TimestampLogger extends Logger{
        abstract override def log(msg:String){
                super.log(new java.util.Date() + " " + msg)
        }
}

特质中的字段

特质中的字段可以是具体的,也可以是抽象的。如果给出了字段的初始值,那么该字段就是具体的。如果没有给出初始值,那么就是抽象的。抽象字段在具体的子类中必须被重写。重写的时候不需要使用 override 关键字。

特质构造顺序

和类一样,特质也有构造器,由字段的初始化和其他特质体中的语句构成。

构造器的执行顺序如下:

  • 首先调用超类的构造器
  • 特质构造器在超类构造器之后,类构造器之前执行
  • 特质由左至右被构造
  • 每个特质中,父特质首先被构造
  • 如果多个特质共有一个父特质,而该父特质已经被构造,则不会再次构造
  • 所有特质构造完,子类被构造

初始化特质中的字段

特质不能有构造参数,每个特质都有一个无参数的构造器。

初始化特质中的字段有两种方法:

  • 提前定义。
val act = new {

        val filename = "a.jpg"

} with SaveAccount with FileLogger
  • 在FileLogger构造器中使用懒值:
trait FileLogger extends Logger{

        val filename:String

        lazy val out = new PrintStream(filename)

        def log(msg:String) { out.println(msg)}
}

扩展类的特质

特质也可以扩展类,这个类会自动成为所有混入该特质的超类。

特质的超类也自动成为我们类的超类。

自身类型

当特质以以下代码开始定义时:

this: 类型 =>

它便只能混入指定类型的子类

trait LoggedException extends Logged{
        this:Exception =>

        def log(){log(getMessage()}
}

注意该特质并不扩展 Exception 类,而是有一个自身类型 Exception 。这意味着,它只能被混入 Exception 子类。

在特质的方法中,我们可以调用该自身类型的任何方法。

第十一章 操作符

标识符

变量、函数、类等名称统称为标识符。但是和Java不一样,scala标识符可以使用任意序列的操作符字符,还可以在反引号中包含几乎任何字符序列。

中置操作符

a 标识符  b

这样的表达式叫做中置表达式。

一元操作符

只有一个参数的操作符称之为一元操作符。如果它出现在一个参数之后,那么它就是一个后置操作符。例如:

a 标识符

上述表达式等同于调用 a.标识符()

以下四个操作符:

  • +
  • -
  • !
  • ~

称之为前置操作符,出现在参数之前,它们被转换为对名为 unary_ 操作符的方法调用。

赋值操作符

赋值操作符的名称形式为 操作符= ,以下表达式:

a 操作符= b

等价于

a = a 操作符 b

优先级

优先级由操作符的首字符决定。出现在同一行字符所产生的操作符优先级相同。

后置操作符的优先级低于中置操作符。

结合性

在scala中,所有操作符都是左结合的。

但是用于构造列表的 :: 操作符是右结合的。

apply和update方法

在scala中以下函数调用语法:

f(arg1, arg2,...)

等价于:

f.apply(arg1, arg2,...)

而以下语法:

f(arg1, arg2,...) = value

等价于

f.update(arg1, arg2, ..., value)

apply 方法也经常用于伴生对象中,用来构造对象而不是显示使用 new 方法。

提取器

提取器就是一个带有 unapply 方法的对象,可以将 unapply 方法看做 apply 方法的逆向操作。

apply 方法接收构造参数,然后将它们转换成对象,而 unapply 方法接收一个对象,然后从中提取值。

unapply 方法返回一个 Option ,它包含一个元组,每个匹配的变量各有一个值与之相对应。

每个样式类都自动具备 apply 方法和 unapply 方法。

第十二章 高阶函数

作为值的函数

可以在变量中存放函数,在scala中,无法直操作方法,只能操作函数。

匿名函数

在scala中,不需要给每一个函数命名。

带函数参数的函数

带函数参数的函数称之为高阶函数。

参数类型推断

当你将一个匿名函数传递给另一个函数或方法时,scala会尽可能帮助你推断出类型信息。

对于只有一个参数的函数,可以省略参数外围的 ()

如果参数只在 => 右侧出现一次,则可以用 _ 替代它。

注意这些简写方法只有在参数类型已知的情况下才有效。

闭包

SAM转换

柯里化

控制抽象

return表达式

第十三章 集合

主要的集合特质

所有的集合都继承了 Iterable 特质。

每个scala集合特质H或类都有一个带有 apply 方法的伴生对象,这个 apply 方法可以用来构建该集合中的实例。

可变和不可变集合

Scala支持可变集合和不可变集合。Scala优先使用不可变集合。

序列

VectorArrayBuffer 的不可变版本,一个带下标的序列,支持快速的随机访问。

Range 表示一个整数序列, 可以使用 tountil 构造 Range 对象。

列表

列表与数组类似,所有成员的类型都相同,但是列表是不可变的,因此不能直接赋值。

// List of Strings
val fruit: List[String] = List("apples", "oranges", "pears")

// List of Integers
val nums: List[Int] = List(1, 2, 3, 4)

// Empty List.
val empty: List[Nothing] = List()

// Two dimensional list
val dim: List[List[Int]] =
        List(
                List(1, 0, 0),
                list(0, 1, 0),
                List(0, 0, 1)
)

在Scala中,列表要么是 Nil ,要么是一个 head 元素加 tail ,而 tail 又是一个列表。

使用 :: 操作符从给定的头到尾创建一个新列表。注意 :: 是右结合的。

// List of Strings
val fruit = "apples" :: ("oranges" :: ("pears" :: Nil))

// List of Integers
val nums = 1 :: (2 :: (3 :: (4 :: Nil)))

// Empty List.
val empty = Nil

可变列表

可变的 LinkedList 和不可变的 List 相似,只不过你可以通过 elem 引用赋值来修改其头部,对 next 引用赋值来修改其尾部。

集是不重复元素的集合,集不保存元素插入的顺序。

LinkedHashSet 可以记住元素被插入的顺序,它会维护一个链表来达到目的。 如果你想要按照已排序的顺序来访问集合中的元素,可以使用 SortedSet

位集是集的另一种实现,以一个字位序列的方式存放非负整数。

contains 方法检查某个集是否包含给定的值,subsetOf 方法检查某个集中所有元素是否被另一个集合包含。

unionintersectdiff 方法执行通常的集操作,也可以将它们写成 |、&、&~union 还可以写成 ++diff 还可以写成 --

用于添加或者去除元素的集合

+ 用于将元素添加至无先后次序的集合,而 +::+ 用于将元素添加至有先后次序的集合的开头或者末尾。

这些操作符都会返回新的集合,不会修改原有的集合。

将函数映射到集合

map 方法可以将某个函数应用到集合中的每个元素并产生其求结果的集合。

如果函数产生一个集合而不是单个值的话,你可能想将所有的值串接在一起,这时可以使用 flatMap

def ulcase(s:String) = Vector(s.toUpperCase(), s.toLowerCase())
#names.map(ulcase)得到

List(Vector("PETER","peter"), Vector("PAUL","paul"), Vector("MARY","mary"))

# names.flatMap(ulcase)得到
List("PETER","peter","PAUL","paul","MARY","mary")

collect 方法用于 partial function ,那些并没有对所有可能的输入值进行定义的函数, 产出被定义的所有参数的函数值得集合

"-3+4".collect(case '+' -> 1; case '-' -> -1) // vector(-1,1)

化简、折叠和扫描

reduceLeft 用于从左至右对元素执行操作。

reduceRight 用于从右至左对元素进行操作。

拉链操作

zip 方法用于将两个集合中的元素组合成一个对偶的列表。

迭代器

通过迭代器,可以使用 nexthasNext 方法遍历集合元素。

迭代器很脆弱,每次调用 next 都会改变迭代器的指向, 流提供了一个不可变得替代品,流的尾部是一个被懒计算的不可变列表,也就是说只有需要的时候才会被计算。

第十四章 模式匹配和样式类

模式匹配

模式可能出现的一个地方就是模式匹配表达式(pattern matching expression): 一个表达式 e ,后面跟着关键字 match 以及一个代码块,这个代码块包含了一些匹配样例; 而样例又包含了 case 关键字、模式、可选的 守卫分句(guard clause) ,以及最右边的代码块; 如果模式匹配成功,这个代码块就会执行。

var sign = ....
val ch:Char = ...
ch match{
    case '+' => sign = 1
    case '-' => sign = -1
    case _ -> sign = 0
}

_ 等效于 default 。如果没有模式匹配,代码会抛出 MatchError

switch 语句不同,scala模式匹配并不会有意外掉入到下一个分支的问题。

if 类似,match 是表达式,而不是语句。你可以在 match 表达式中使用任何类型,而不仅仅是数字。

守卫

假如我们需要扩展我们的示例以匹配所有数字,在Java中,只能简单的添加多个 case 标签,而在scala中,可以使用守卫。

ch match {
    case '+' => sign = 1
    case '-' => sign = -1
    case _ if Character.isDigit(ch) => digit = Character.digit(ch, 10)
    case _  => sign = 0
}

守卫可以是任意 Boolean 条件。

模式中的变量

如果关键字 case 后面跟着一个变量名,那么匹配的表达式会被赋值给该变量。

str(i) match{
    case '+' => sign = 1
    case '-' => sign = -1
    case ch => digit = Character.digit(ch, 10)
}

也可以在守卫中使用变量。

注意变量模式可能与常量表达式相冲突。所以变量名必须以小写开头,如果你有一个小写字母开头的常量,则需要将它包在反引号中。

类型模式

还可以对表达式的类型进行匹配:

obj match{
    case x:Int => x
    case s:String => Integer.parseInt(s)
    case _:BigInt => Int.MaxValue
    case _ => 0
}

在scala中,我们更倾向于使用模式匹配,而不是 isInstanceOf 操作符。

当你在匹配类型的时候,必须给出一个变量名,否则,你将会拿对象的本身进行匹配。

匹配发生在运行期,Java虚拟机中泛型的类信息会被擦掉,因此,不能用类型来匹配特定的 Map 类型。

case m:Map[String, Int] => ....   //禁止这样做

不过,可以匹配一个通用的映射:

case m:Map[_,_] => ... //Ok

但是对于数组而言,元素的类型信息是完好的,因此可以匹配 Array[Int]

匹配数组、列表和元组

要匹配数组的内容,可以在模式中使用 Array 表达式,例如:

arr match{
    case Array(0) => "0"
    case Array(x,y) => x + "" + y
    case Array(0, _*) => "0 ..."
    case _ => "something else"
}

第一个模式匹配包含0的数组,第二个模式匹配任何带有两个元素的数组,并将这两个元素分别绑定到变量x和y。第三个表达式匹配任何以0开始的数组。

你也可以用同样的方式匹配列表,使用List表达式,或者使用 :: 操作符。

lst match{
    case 0::Nil => "0"
    case x::y::Nil => x +"" + y
    case 0::tail => "0..."
    case _ => "something else"
}

对于元组,可以在模式中使用元组表示法:

pair match{
    case (0, _) => "0..."
    case (y, _) => y + "0"
    case _ => "neither is 0"
}

提取器

Scala 提取器是一个带有 unapply 方法的对象。 unapply 方法算是 apply 方法的反向操作: unapply 接受一个对象,然后从对象中提取值,提取的值通常是用来构造该对象的值。

模式匹配列表、数组或元组背后的机制其实是提取器。 当我们在提取器对象中使用 match 语句是, unapply 将自动执行。

正则表达式是另一个适合使用提取器的场景,如果正则表达式有分组,可以使用提取器来匹配分组。

例如:

val pattern = "([0-9]+)([a-z]+)".r
"99 bottles" match{
    case pattern(num, item)=>...
}

变量声明中的模式

在变量声明中也可以使用模式。

val (x,y) = (1,2)

for推导式中的模式

你可以在 for 推导式中使用带变量的模式,对每一个遍历的值,这些变量都会被绑定。

for 推导式中,失败的匹配将会被安静的忽略。

样式类

样式类是一种特殊的类,它经过优化以用于模式匹配。

当声明样式类的时候,自动做以下几件事:

  • 构造器中的每一个参数都成为 val
  • 在伴生对象中提供 new 方法,,所以可以不使用 new 关键字就可构建对象;
  • 提供 unapply 方法使模式匹配可以工作;
  • 生成 toStringequalshashCodecopy 方法,除非显示给出这些方法的定义。

copy方法和带名参数

样式类的copy方法创建一个与现有对象值相同的对象。

case语句中的中置表示法

如果 unsupply 方法产生一个对偶,则你可以在 case 语句中使用中置表示法,尤其是对于有两个参数的样式类,可以使用中置表示法来表示。

第十五章 文件和正则表达式

读取行

要读取文件中的所有行,可以调用 scala.io.Source 对象的 getLines 方法。

import scala.io.Source
val source = Source.fromFile("a.txt", "UTF-8")
val lineIterator = source.getLines

结果是一个迭代器。可以用来逐条处理这些行。或者也可以对迭代器应用 toArray``toBuffer``方法。

有时候,你只想将整个文件读取成一个字符串,这更加简单:

val contents = source.mkString

在调用完 Source 对象之后,记得 close

读取字符

如果需要读取单个字符,可以直接把 Source 对象当作迭代器,因为 Source 类扩展自 Iterator[Char] :

for(c <- source) //处理c

从URL或者其他源读取

Source 对象有读取非文件源的方法:

val source1 = Source.fromURL("http://www.baidu.com")
val source2 = Source.fromString("hello,world")
val source3 = Source.stdin

读取二进制文件

Scala没有提供读取二进制的方法,需要使用Java类库。

val file = new File(filename)
val in = new FileInputStream(file)
var bytes = new Array[Byte](file.length.toInt)
in.read(bytes)
in.close()

写入文本文件

一样没有提供内建方法,需要使用Java类库。

import java.io.PrintWriter
val out = new PrintWriter("numbers.txt")
for(i <- 1 to 10) out.println(i)
out.close()

序列化

首先声明一个可序列化的类:

@SerialVersionUID(42L) classs Person extends Serializable

Serializable 特质定义在scala包中,因此不需要显示导入。

如果你能接受缺省的ID,也可以省略掉 @SerialVersionUID 注解。

Scala集合类都是可序列化的,因此可以把他们用作可序列化类的成员。

进程控制

scala提供了 scala.sys.process 包用于与 shell 交互。

import sys.process._
"ls -al "!

正则表达式

要构造一个 Regex 对象,用 String 类的 r 方法即可。

val pattern = "[0-9]+".r

如果正则表达式包含了反斜杠或者引号的话,最好使用原始字符串的形式 """...""""

findAllIn 方法返回遍历所有匹配项的迭代器。

使用 toArray 可以将迭代器转换成数组。

要想找到字符串中的首个匹配项,可以使用 findFirstIn ,得到的结果是一个 Option[String]

使用 replaceFirstIn 或者 replaceAllIn 替换首个或者全部匹配字符串。

正则表达式组

分组可以让我们方便的获取正则表达式的子表达式。

val numItemPattern = "([0-9]+) ([a-z]+)".r
val numItemPattern(num, item) = "99 bottles"

第十六章 注解

什么是注解

注解是那些你插入到代码中以便有工具可以对它们进行处理的标签。

注解的语法和Java一样,可以对scala类使用Java注解,也可以使用Scala注解,scala注解是scala独有的。Java注解不影响编译器如何将源码编译成字节码,而Scala可以影响到编译过程。

什么可以被注解

在scala中,你可以为类、方法、字段、局部变量和参数添加注解,和Java一样。

@Entity class Credentials
@Test def test(){}
@BeanProperty var username = _
def doSomething(@NotNull message:String){}

也可以同时添加多个注解,先后次序没有影响。

@BeanProperty @Id var username = _

在给主构造器添加注解的时候,需要将注解放置在构造器之前,并加上一对圆括号(如果注解不带参数的话)

class Credential @inject() (var username:String, var password:String)

还可以为表达式添加注解,需要在表达式后面添加冒号,然后注解本身。

也可以为类型参数添加注解:

class MyContainer[@specialized T]

针对实际类型的注解应该放在类型名称之后:

String @cps[Unit]

注解参数

Java注解可以有带名参数:

@Test(timeout=100, expected=classOf[IOException])

如果参数名为 Value ,则参数名可以直接省略。如果注解不带参数,则圆括号可以省去。

@Named("credt") var credentials:Credentials = _

大多数注解参数都有缺省值。

Java注解的参数类型只能为:

  • 数值型字面量
  • 字符串
  • 类字面量
  • Java枚举
  • 其他注解

scala注解可以是任何类型。

注解实现

注解必须扩展 Annotation 特质。

针对Java的注解

Java修饰符

第十七章 Try与异常处理

异常的抛出和捕获

Scala中使用 throw 语句抛出异常:

case class Customer(age: Int)
class Cigarettes
case class UnderAgeException(message: String) extends Exception(message)
def buyCigarettes(customer: Customer): Cigarettes =
  if (customer.age < 16)
    throw UnderAgeException(s"Customer must be older than 16 but was ${customer.age}")
  else new Cigarettes

被抛出的异常能够以类似 Java 中的方式被捕获,虽然是使用偏函数来指定要处理的异常类型。 此外, Scala 的 try/catch 是表达式(返回一个值),因此下面的代码会返回异常的消息:

val youngCustomer = Customer(15)
try {
  buyCigarettes(youngCustomer)
  "Yo, here are your cancer sticks! Happy smokin'!"
} catch {
    case UnderAgeException(msg) => msg
}

此外,还可以使用 finally 语句。

函数式的错误处理

Scala 使用一个特定的类型来表示可能会导致异常的计算,这个类型就是 Try

Try的语义

解释 Try 最好的方式是将它与上一章所讲的 Option 作对比。

Option[A] 是一个可能有值也可能没值的容器, Try[A] 则表示一种计算: 这种计算在成功的情况下,返回类型为 A 的值,在出错的情况下,返回 Throwable 。 这种可以容纳错误的容器可以很轻易的在并发执行的程序之间传递。

Try 有两个子类型:

  • Success[A] :代表成功的计算。
  • 封装了 ThrowableFailure[A] :代表出了错的计算。

如果知道一个计算可能导致错误,我们可以简单的使用 Try[A] 作为函数的返回类型。 这使得出错的可能性变得很明确,而且强制客户端以某种方式处理出错的可能。

假设,需要实现一个简单的网页爬取器:用户能够输入想爬取的网页 URL , 程序就需要去分析 URL 输入,并从中创建一个 java.net.URL

import scala.util.Try
import java.net.URL
def parseURL(url: String): Try[URL] = Try(new URL(url))

正如你所看到的,函数返回类型为 Try[URL]: 如果给定的 url 语法正确,这将是 Success[URL], 否则, URL 构造器会引发 MalformedURLException ,从而返回值变成 Failure[URL] 类型。

上例中,我们还用了 Try 伴生对象里的 apply 工厂方法,这个方法接受一个类型为 A 的 传名参数, 这意味着, new URL(url) 是在 Tryapply 方法里执行的。

apply 方法不会捕获任何非致命的异常,仅仅返回一个包含相关异常的 Failure 实例。

使用Try

使用 Try 与使用 Option 非常相似,在这里你看不到太多新的东西。

你可以调用 isSuccess 方法来检查一个 Try 是否成功,然后通过 get 方法获取它的值, 但是,这种方式的使用并不多见,因为你可以用 getOrElse 方法给 Try 提供一个默认值:

val url = parseURL(Console.readLine("URL: ")) getOrElse new URL("http://duckduckgo.com")

如果用户提供的 URL 格式不正确,我们就使用 DuckDuckGoURL 作为备用。

模式匹配

SuccessFailure 类都是样式类,所以 Try 也支持模式匹配:

import scala.util.Success
import scala.util.Failure
getURLContent("http://danielwestheide.com/foobar") match {
  case Success(lines) => lines.foreach(println)
  case Failure(ex) => println(s"Problem rendering URL content: ${ex.getMessage}")
}

从故障中恢复

如果想在失败的情况下执行某种动作,没必要去使用 getOrElse , 一个更好的选择是 recover ,它接受一个偏函数,并返回另一个 Try 。 如果 recover 是在 Success 实例上调用的,那么就直接返回这个实例,否则就调用偏函数。 如果偏函数为给定的 Failure 定义了处理动作, recover 会返回 Success ,里面包含偏函数运行得出的结果。

下面是应用了 recover 的代码:

import java.net.MalformedURLException
import java.io.FileNotFoundException
val content = getURLContent("garbage") recover {
 case e: FileNotFoundException => Iterator("Requested page does not exist")
 case e: MalformedURLException => Iterator("Please make sure to enter a valid URL")
 case _ => Iterator("An unexpected error has occurred. We are so sorry!")
}

第十八章 Either类型

Either 语义

Either 也是一个容器类型,但不同于 TryOption ,它需要两个类型参数: Either[A, B] 要么包含一个类型为 A 的实例, 要么包含一个类型为 B 的实例。 这和 Tuple2[A, B] 不一样, Tuple2[A, B] 是两者都要包含。

Either 只有两个子类型: LeftRight , 如果 Either[A, B] 对象包含的是 A 的实例, 那它就是 Left 实例,否则就是 Right 实例。

在语义上,Either 并没有指定哪个子类型代表错误,哪个代表成功, 毕竟,它是一种通用的类型,适用于可能会出现两种结果的场景。 而异常处理只不过是其一种常见的使用场景而已, 不过,按照约定,处理异常时, Left 代表出错的情况, Right 代表成功的情况。

创建 Either

创建 Either 实例非常容易, LeftRight 都是样例类。 要是想实现一个 “坚如磐石” 的互联网审查程序,可以直接这么做:

import scala.io.Source
import java.net.URL
def getContent(url: URL): Either[String, Source] =
 if(url.getHost.contains("google"))
   Left("Requested URL is blocked for the good of the people!")
 else
  Right(Source.fromURL(url))

调用 getContent(new URL("http://danielwestheide.com")) 会得到一个封装有 scala.io.Source 实例的 Right, 传入 new URL("https://plus.google.com") 会得到一个含有 StringLeft

Either 用法

Either 基本的使用方法和 OptionTry 一样: 调用 isLeft (或 isRight )方法询问一个 Either ,判断它是 Left 值, 还是 Right 值。 可以使用模式匹配,这是最方便也是最为熟悉的一种方法:

getContent(new URL("http://google.com")) match {
 case Left(msg) => println(msg)
 case Right(source) => source.getLines.foreach(println)
}

第十九章 隐式转换与隐式参数

隐式转换

隐式转换函数

implicit 关键字声明的带有单个参数的函数,这样的函数会自动被应用,将值从一种类型装换成另一种类型。

例如,将整数n转换成分数n/1:

implicit def int2Fraction(n:Int) = Fraction(n)

这样就可以对如下表达式进行求值了。

var result = 3 * Fraction(4,5)

由于我们并不会显示调用这个函数,所以可以给函数取任意名字。

我们可以使用隐式转换丰富现有类库的功能。

隐式类

隐式类支持在主构造函数中进行隐式转换。隐式类用 implicit 注明,它必须位于 classobject 或者特质中,不能为顶级类。

它的语法格式如下所示:

object <object name> {
   implicit class <class name>(<Variable>: Data type)
   {
       def <method>(): Unit=
   }
}

下面在Run.scala中定义一个隐式类 IntTimes ,类中包含方法 times()

object Run {

    implicit class IntTimes(x: Int) {

        def times [A](f: =>A): Unit={

           def loop(current: Int): Unit=if(current > 0){
                f
                loop(current - 1)
           }
           loop(x)
        }
   }
}

然后在Demo.run中输入以下代码:

import Run._

object Demo {
   def main(args: Array[String]) {
        4 times println("hello")
    }
}

当执行 4 times printl(""hello") 时会自动执行隐式转换,将 4 转换为 IntTimes 的实例,然后调用 times 方法。

隐式类有以下限制条件:

  • 只能在别的trait/类/对象内部定义。
  • 构造函数只能携带一个非隐式参数。
  • 在同一作用域内,不能有任何方法、成员或对象与隐式类同名。这意味着隐式类不能是case class。

引入隐式转换

有三种方法引入隐式转换:

  • 将隐式函数放置于源或者目标类型的伴生对象中。
  • 将隐式函数置于当前作用域中,使用 implicit 关键字声明。
  • 从其他包中引入隐式函数。

在REPL中使用 :implicits 查看所有除 Predef 外被引用的隐式成员。

隐式转换规则

以下情况下会进行隐式转换:

  • 当表达式的类型与预期的类型不同时;
  • 当对象访问一个不存在的成员时;
  • 当对象调用某个方法,而该方法的参数声明与传入参数不匹配时。

以下情况不会进行隐式转换:

  • 如果代码能够在不使用隐式转换的前提下通过编译,则不会使用隐式转换;
  • 编译器不会尝试同时执行多个转换;
  • 存在二义性的转换是个错误。

隐式参数

函数或者方法可以带有一个标记为 implicit 的参数列表,这种情况下,编译器将会查找缺省值,提供给该函数或者方法。

例如:

case class Delimiter(left:String, right:String)

def quote(what:String)(implicit delims:Delimiter) ={
    delims.left + what + delims.right
}

我们可以使用一个显示的Delimiter对象来调用quote方法,就像这样:

quote("hello")(Delimiter("left", "right")

也可以省略隐式参数列表。

quote("hello")

在这种情况下,编译器会查找一个类型为Delimiter的隐式值,这个隐式值必须被声明为implicit的值,编译器会尝试在以下两个地方查找这样的一个对象。

  • 在当前作用域所有可以用单个标识符指代的满足类型要求的val和def
  • 与所要求类型相关联的类型的伴生对象。相关联的类型包括所要求类型本身,以及它的类型参数。

第二十章 Future 和Promise

Future 提供了一套高效便捷的非阻塞并行操作管理方案。其基本思想很简单,所谓 Future ,指的是一类占位符对象,用于指代某些尚未完成的计算的结果。一般来说,由 Future 指代的计算都是并行执行的,计算完毕后可另行获取相关计算结果。 以这种方式组织并行任务,便可以写出高效、异步、非阻塞的并行代码。

默认情况下,FuturePromise 并不采用一般的阻塞操作,而是依赖回调进行非阻塞操作。为了在语法和概念层面更加简明扼要地使用这些回调,Scala还提供了 flatMapforeachfilter 等函数,使得我们能够以非阻塞的方式对 Future 进行组合。当然, ``Future``仍然支持阻塞操作——必要时,可以阻塞等待 ``Future``(不过并不鼓励这样做)。

一个典型的 Future 示例如下:

val inverseFuture: Future[Matrix] = Future {
    fatMatrix.inverse() // non-blocking long lasting computation
}(executionContext)

或者使用更加通用的写法:

上面的代码将 fatMatrix.inverse() 放在 ExecutionContext 中执行,然后将计算结果放在 inverseFuture 中。

执行上下文

执行上下文

FuturePromise 的计算任务放在 ExecutionContext 中执行。一个 ExecutionContext 相当于一个执行器,它会将计算任务放在新线程或者线程池中执行,也可以放在当前线程中执行,不过不推荐。

scala.concurrent 提供了一个 ExecutionContext 的实现,一个全局的静态线程池,它也可以将 Executor 转换成 ExecutionContext

当然我们也可以通过继承 ExecutionContext 来自定义执行上下文。

全局执行上下文

ExecutionContext.globalForkJoinPool 提供,它能满足我们大多数情况下的需求,但是也需要注意一些情况。

Future

所谓 Future ,是一种用于指代某个尚未就绪的值的对象。而这个值,往往是某个计算过程的结果:

  • 若该计算过程尚未完成,我们就说该 ``Future``未就位;
  • 若该计算过程正常结束,或中途抛出异常,我们就说该 Future 已就位。

Future 的就位分为两种情况:

  • Future 带着某个值就位时,我们就说该 Future 携带计算结果成功就位。
  • Future 因对应计算过程抛出异常而就绪,我们就说这个 Future 因该异常而失败。

Future 的一个重要属性在于它只能被赋值一次。一旦给定了某个值或某个异常,Future 对象就变成了不可变对象——无法再被改写。

创建 Future 对象最简单的方法是调用 Future.apply 方法,该方法启用异步(asynchronous)计算并返回保存有计算结果的 Futrue , 一旦计算完成,Future 中的结果就变的可用。

注意 Future[T] 是表示 Future 对象的类型,而 Future.apply 是方法,该方法创建和调度一个异步计算,并返回随着计算结果而完成的 Future 对象。

这最好通过一个例子予以说明。

假设我们使用某些流行的社交网络的假定API获取某个用户的朋友列表,我们将打开一个新对话(session),然后发送一个请求来获取某个特定用户的好友列表。

import scala.concurrent._
import ExecutionContext.Implicits.global
val session = socialNetwork.createSessionFor("user", credentials)
val f: Future[List[Friend]] = Future {
  session.getFriends()
}

以上,首先导入 scala.concurrent 包使得 Future 类型和 future 构造函数可见。我们将等会解释第二个导入。

然后我们初始化一个 session 变量来用作向服务器发送请求,用一个假想的 createSessionFor 方法来返回一个 List[Friend] 。为了获得朋友列表,我们必须通过网络发送一个请求,这个请求可能耗时很长。这能从调用`getFriends`方法得到解释。为了更好的利用CPU,响应到达前不应该阻塞(block)程序的其他部分执行,于是在计算中使用异步。`Future`方法就是这样做的,它并行地执行指定的计算块,在这个例子中是向服务器发送请求和等待响应。

一旦服务器响应, f 中的好友列表将变得可用。

未成功的尝试可能会导致一个异常(exception)。在下面的例子中, session 的值未被正确的初始化, 于是在 Future 的计算中将抛出 NullPointerException ,future f 不会圆满完成,而是以此异常失败。

val session = null
val f: Future[List[Friend]] = Future {
  session.getFriends
}

import ExecutionContext.Implicits.global 导入默认的全局执行上下文(global execution context),执行上下文执行执行提交给他们的任务,也可把执行上下文看作线程池,这对于`Future`方法来说是必不可少的,因为这可以处理异步计算如何及何时被执行。我们可以定义自己的执行上下文,并在Future上使用它,但是现在只需要知道你能够通过上面的语句导入默认执行上下文就足够了。

我们的例子是基于一个假定的社交网络API,此API的计算包含发送网络请求和等待响应。提供一个涉及到你能试着立即使用的异步计算的例子是公平的。假设你有一个文本文件,你想找出一个特定的关键字第一次出现的位置。当磁盘正在检索此文件内容时,这种计算可能会陷入阻塞,因此并行的执行该操作和程序的其他部分是合理的(make sense)。

val firstOccurrence: Future[Int] = Future {
  val source = scala.io.Source.fromFile("myText.txt")
  source.toSeq.indexOfSlice("myKeyword")
}

Callback

现在我们已经知道了如何创建 Future ,那么怎么获取 Future 中的计算结果呢?

在许多 Future 的实现中,一旦 Future 的用户对 Future 的结果感兴趣,它不得不阻塞它自己的计算直到 Future 完成, 然后才能使用 Future 的结果,然后继续它自己的计算。虽然这在Scala的 Future API (在后面会展示)中是允许的,但是从性能的角度来看更好的办法是一种完全非阻塞的方法,即在Future中注册一个回调。

注册回调函数最通常的形式是使用 OnComplete 方法,即创建一个 Try[T] => U 类型的回调函数。如果 Future 成功完成, 回调则会应用到 Success[T] 类型的值中,否则应用到 Failure[T] 类型的值中。

Try[T]Option[T]Either[T, S] 相似,因为它是一个可能持有某种类型值的单值容器。然而,它被特意设计用来保存一个值或一个 throwable对象。Option[T] 既可以是一个值(如:Some[T])也可以是无值(如:None),如果`Try[T]`获得一个值则它为`Success[T]` ,否则为`Failure[T]`的异常。 Failure[T]`可以获取错误信息,而不仅仅是None。同时也可以把`Try[T]`看作一种特殊版本的`Either[Throwable, T],专门用于左值为可抛出类型(Throwable)的情形。

回到我们的社交网络的例子,假设我们想要获取我们最近的帖子并显示在屏幕上, 我们通过调用 getRecentPosts 方法获得一个返回值 List[String] ——一个近期帖子的列表文本:

import scala.util.{Success, Failure}
val f: Future[List[String]] = Future {
  session.getRecentPosts
}
f onComplete {
  case Success(posts) => for (post <- posts) println(post)
  case Failure(t) => println("An error has occured: " + t.getMessage)
}

onComplete 方法允许客户同时处理成功和失败的情况。如果仅需处理成功的情况,可以使用 onSuccess 回调函数使(该回调以一个偏函数(partial function)为参数):

val f: Future[List[String]] = Future {
  session.getRecentPosts
}
f onSuccess {
  case posts => for (post <- posts) println(post)
}

如果只处理失败情况,可以使用 onFailure 回调函数:

val f: Future[List[String]] = Future {
  session.getRecentPosts
}
f onFailure {
  case t => println("An error has occured: " + t.getMessage)
}
f onSuccess {
  case posts => for (post <- posts) println(post)
}

如果任务失败,即 Future 抛出异常,则执行 onFailure 回调函数。

由于偏函数具有 isDefinedAt 方法, onFailure 回调函数只有在特定的 Throwable 类型对象下会触发。下面例子中的 onFailure 回调永远不会被触发:

val f = Future {
  2 / 0
}
f onFailure {
  case npe: NullPointerException =>
    println("I'd be amazed if this printed out.")
}

回到前面查找关键字的例子,我们可能想在屏幕上打印出此关键字的位置:

val firstOccurrence: Future[Int] = Future {
  val source = scala.io.Source.fromFile("myText.txt")
  source.toSeq.indexOfSlice("myKeyword")
}
firstOccurrence onSuccess {
  case idx => println("The keyword first appears at position: " + idx)
}
firstOccurrence onFailure {
  case t => println("Could not process file: " + t.getMessage)
}

回调函数 onCompleteonSuccessonFailure 的返回结果为 Unit 类型,也就是说这些函数并不支持链式调用。

注意回调函数并不一定是由返回计算结果的线程调用,也不一定是由创建回调函数的线程来调用,只能说是由某个线程来调用。

而且回调函数的执行顺序不是固定的,实际上,回调函数不一定是顺序调用,也可能是并发执行的。

看下面的例子:

@volatile var totalA = 0
val text = Future {
  "na" * 16 + "BATMAN!!!"
}
text onSuccess {
  case txt => totalA += txt.count(_ == 'a')
}
text onSuccess {
  case txt => totalA += txt.count(_ == 'A')
}

上面的例子中,两个回调函数如果是顺序执行的话,text的结果为18,但是也可能并发执行,这时结果可能为16或者2。

回调函数的相关规则:

  • onComplete 不管结果是否成功或者失败都会执行
  • onSuccess 只有成功才会执行
  • onFailure 只有失败才会执行
  • 回调的执行顺序不是固定的,除非使用自定义的 ExecutionContext
  • 在一些回调抛出异常的情况下,其他回调的执行不受影响
  • 在某些情况下一些回调可能永远不能结束,导致其他回调不会执行,这时需要使用阻塞回调
  • 一旦执行完,回调将从future对象中移除,这样更适合JVM的垃圾回收机制(GC)。

函数组合与For表达式

使用回调函数有可能导致回调地狱,例如下面这个例子,假设我们通过API与货币交易系统交互,当有利可图的时候就购入美元:

val rateQuote = Future {
 connection.getCurrentValue(USD)
}
rateQuote onSuccess { case quote =>
  val purchase = Future {
   if (isProfitable(quote)) connection.buy(amount, quote)
   else throw new Exception("not profitable")
 }

 purchase onSuccess {
    case _ => println("Purchased " + amount + " USD")
  }
}

上面的代码能够正常运行,但是并不合适,主要原因如下:一是回调函数嵌套调用,可阅读性差;二是 purchase 变量在其它的代码的作用域中不可见;

为了解决上述问题,Future提供了组合器combinators。最基础的就是 map ,它接收一个future对象, 然后创建一个新的future对象然后返回,它包含了成功计算的结果。你可以像理解容器(collections)的 map 一样来理解future的 map

现在重写上面的代码:

val rateQuote = Future {
 connection.getCurrentValue(USD)
}
val purchase = rateQuote map { quote =>
  if (isProfitable(quote)) connection.buy(amount, quote)
  else throw new Exception("not profitable")
}
purchase onSuccess {
  case _ => println("Purchased " + amount + " USD")
}

通过使用 map 我们消除了一个回调,更重要的是消除了嵌套。

如果 isProfitable 返回 false , 结果导致异常抛出,也就没有值来进行映射,所以导致 purchase 抛出的异常与 rateQuote 的异常一致。

总之,如果原 Future 的计算成功完成了,那么返回的 Future 将会使用原 Future 的映射值来完成计算。如果映射函数抛出了异常则 Future 也会带着该异常完成计算。如果原 Future 由于异常而计算失败,那么返回的 Future 也会包含相同的异常。这种异常的传导方式也同样适用于其他的组合器(combinators)。

Future 的设计目标之一就是让它们支持 for 表达式,Future 还支持 flatMap , filter , foreach 等组合器。

其中 flatMap 方法可以构造一个函数,它可以把值映射到一个姑且称为 g 的新 future ,然后返回一个随 g 的完成而完成的 Future 对象。

假设我们现在需要交易美元和瑞士币,首先需要获取它们各自的报价,然后在这两个报价的基础上进行交易:

val usdQuote = Future { connection.getCurrentValue(USD) }
val chfQuote = Future { connection.getCurrentValue(CHF) }
val purchase = for {
  usd <- usdQuote
  chf <- chfQuote
  if isProfitable(usd, chf)
} yield connection.buy(amount, chf)
purchase onSuccess {
  case _ => println("Purchased " + amount + " CHF")
}

上面的for表达式也可以转换成:

val purchase = usdQuote flatMap {
  usd =>
  chfQuote
    .withFilter(chf => isProfitable(usd, chf))
    .map(chf => connection.buy(amount, chf))
}

Future 还提供了 recover 方法来处理异常:

比方说我们准备在 rateQuote 的基础上决定购入一定量的货币,那么 connection.buy 方法需要知道购入的数量和期望的报价值,最终完成购买的数量将会被返回。假如报价值偏偏在这个节骨眼儿改变了,那buy方法将会抛出一个 QuoteChangedExecption ,并且不会做任何交易。如果我们想让我们的 Future 对象返回0而不是抛出那个该死的异常,那我们需要使用 recover 组合器:

val purchase: Future[Int] = rateQuote map {
  quote => connection.buy(amount, quote)
  } recover {
  case QuoteChangedException() => 0
}

recover 能够创建一个新 future 对象,该对象当计算完成时持有和原 future 对象一样的值。

Future (6 / 0) recover { case e: ArithmeticException => 0 } // result: 0
Future (6 / 0) recover { case e: NotFoundException   => 0 } // result: exception
Future (6 / 2) recover { case e: ArithmeticException => 0 } // result: 3

recoverWith 创建一个新的 future 对象,当原``future`` 计算成功时,新的 future 对象包含了成功的计算结果,如果失败或者异常,偏函数将会返回造成原 future 失败的相同的 Throwable 异常, 如果此时 Throwable 又被映射给了别的 future ,那么新 Future 就会完成并返回这个 future 的结果。 recoverWithrecover 的关系跟 flatMapmap 之间的关系很像。

val f = Future { Int.MaxValue }
Future (6 / 0) recoverWith { case e: ArithmeticException => f } // result: Int.MaxValue

fallbackTo 连接两个 future 对象, 如果第一个执行成功,返回第一个的结果;如果第一个失败,继续执行第二个,并返回第二个的结果;如果第一、二个都失败了,则返回第一个计算结果。

val f = Future { sys.error("failed") }
val g = Future { 5 }
val h = f fallbackTo g
Await.result(h, Duration.Zero) // evaluates to 5

andThen 会根据当前 future 的计算结果返回一个新的 future ,这样可以让回调顺序执行。

综上所述, Future 的组合器功能是纯函数式的,每种组合器都会返回一个与原 Future 相关的新 Future 对象。

投影

为了确保 for 解构(for-comprehensions)能够返回异常, futures 也提供了投影(projections)。如果原 future 对象失败了,失败的投影(projection)会返回一个带有 Throwable 类型返回值的 future 对象。如果原 Future 成功了,失败的投影(projection)会抛出一个 NoSuchElementException 异常。下面就是一个在屏幕上打印出异常的例子:

val f = Future {
 2 / 0
}
for (exc <- f.failed) println(exc)

下面的例子不会在屏幕上打印出任何东西:

val f = Future {
 4 / 2
}
for (exc <- f.failed) println(exc)

阻塞和异常

阻塞

Future通常都是异步执行的,而且也不会阻塞底层的线程任务。但是在某些情况下需要阻塞,下面我们讨论两种阻塞线程的方法:

Future中阻塞

在全局的执行上下文中,可以通过 blocking 结构来通知执行上下文是一个阻塞调用。

implicit val ec = ExecutionContext.fromExecutor(
                  Executors.newFixedThreadPool(4))
Future {
 blocking { blockingStuff() }
}

阻塞的代码可能抛出异常,这种情况下,异常会被传递给调用者。

Future外阻塞

前面也提到过,阻塞future并不被鼓励,因为这样容易导致死锁。 回调函数以及组合器是更加推荐的方式,但是阻塞在某些情况下也是必须的。

在前面的货币交易示例中,有一个地方需要用到阻塞,因为它需要等所有的future计算结果完成。

import scala.concurrent._
import scala.concurrent.duration._
def main(args: Array[String]) {
 val rateQuote = Future {
    connection.getCurrentValue(USD)
 }

  val purchase = rateQuote map { quote =>
    if (isProfitable(quote)) connection.buy(amount, quote)
    else throw new Exception("not profitable")
  }

 Await.result(purchase, 0 nanos)
}

使用 Await.results 等待计算完成,如果成功则返回成功的值,如果失败,则抛出 NoSuchElementException

还可以使用 Await.ready , 该方法会等待计算完成,但是并不获取计算结果,也就是说计算失败的时候不会抛出异常。

The Future trait实现了 Awaitable traitready()result() 方法。这些方法不能被客户端直接调用,它们只能通过执行环境上下文来进行调用。

异常

当异步计算抛出未处理的异常时,相应的 futures 就会失败。 失败的 future 包含了一个 Throwable 对象。

下面的异常处理是不一样的:

  • scala.runtime.NonLocalReturnControl[_]
  • ExecutionException

Promise

前面我们介绍了通过 future 方法来创建 Future 对象, 实际上,futures 还可以通过 Promise 来创建。

如果说 futures 是为了一个还没有存在的结果,而当成一种只读占位符的对象类型去创建,那么 promise 就被认为是一个可写的, 可以实现一个 future 的单一赋值容器。这就是说, promise 通过这种 `success 方法可以成功去实现一个带有值的 future 。 相反的,因为一个失败的 promise 通过 failure 方法就会实现一个带有异常的 future

一个 Promise 对象 p 通过调用 p.future 方法返回 future 对象。

考虑下面的生产者消费者模式:

import scala.concurrent.{ Future, Promise }
import scala.concurrent.ExecutionContext.Implicits.global
val p = Promise[T]()
val f = p.future
val producer = Future {
 val r = produceSomething()
  p success r
 continueDoingSomethingUnrelated()
}
val consumer = Future {
  startDoingSomething()
  f onSuccess {
    case r => doSomethingWithResult()
  }
}

上面我们创建了一个 Promise 对象, 然后使用 future 方法获取 Future 对象,然后执行两个异步计算,第一个用于生成,第二个用于消费。

正如前面提到的, promises 具有单赋值语义。因此,它们仅能被实现一次。在一个已经计算完成的 promise 或者 failedpromise 上调用 success 方法将会抛出一个 IllegalStateException 异常。

下面这个例子处理了Promise失败的情况:

val p = promise[T]
val f = p.future
val producer = Future {
  val r = someComputation
  if (isInvalid(r))
    p failure (new IllegalStateException)
  else {
    val q = doSomeMoreComputation(r)
    p success q
  }
}

Promises也能通过一个 complete 方法来实现,返回结果为 Try[T] 类型,这个值要么是一个类型为 Failure[Throwable] 的失败的结果值,要么是一个类型为 Success[T] 的成功的结果值。

类似 success 方法,在一个已经完成(completed)的 promise 对象上调用 failure 方法和 complete 方法同样会抛出一个 IllegalStateException 异常。

completeWith 方法将用另外一个 future 完成 promise 计算。当该 future 结束的时候,该 promise 对象得到那个 ``future``对象同样的值,如下的程序将打印1:

val f = Future { 1 }
val p = promise[Int]
p completeWith f
p.future onSuccess {
  case x => println(x)
}

工具库

为了简化并发开发过程中的时间处理,scala.concurrent 库提供了 Duration 类。

Duration 代表一段时间范围,它既可以代表有限时间,也可以代表无限时间。有限时间通过 FiniteDuration 表示,无限时间只有两种情况 Duration.InfDuration.MinusInf

Duration库还提供了其他一些子类用于隐式参数转换。

Duration类包括以下方法:

  • 时间单位转换函数(toNanos, toMicros, toSeconds, toMinutes, toHours, toDays, toUnits等)
  • 时间大小比较函数(<, <=, >, >=)
  • 算术运算(+,-,*,/, unary_-)
  • 获取两个Duration中最大值max或者最小值min
  • 判断Duration对象是否无限

Duration可以通过以下方法实例化:

  • 通过隐式参数转换,从Int或Long类型转换为Duration
  • 通过一个Long类型的参数,以及一个 java.util.concurrent.TimeUnit 类型的参数构建, 例如:val d = Duration(100, MILLISECONDS)
  • 通过字符串构建,例如 val d = Duration("1.2 µs")

Duration还提供了unapply方法,所以它支持模式匹配:

import scala.concurrent.duration._
import java.util.concurrent.TimeUnit._

// instantiation
val d1 = Duration(100, MILLISECONDS) // from Long and TimeUnit
val d2 = Duration(100, "millis") // from Long and String
val d3 = 100 millis // implicitly from Long, Int or Double
val d4 = Duration("1.2 µs") // from String
// pattern matching
val Duration(length, unit) = 5 millis

Indices and tables