快学Scala

第一章

1.1 Scala解释器

Scala解释器读取到一个表达式,对它进行求值,将它打印出来,接着再继续读下一个表达式。这个过程称作读取-求值-打印-循环,即REPL。

实际发生的是,你输入的内容被快速地编译成字节码,然后这段字节码交由java虚拟机执行。

val定义常量

var定义变量

不需要类型。不够在必要的时候,可以指定类型:

val greeting: String = null
val greeting: Any = "Hello"

变量或函数的类型总是写在变量或函数名称的后面。

无分号,仅当同一行代码中存在多条语句时才需要分号隔开。

val xmax,ymax = 100 // 将xmax和ymax都设置为100 注意和c++不同

七种数据类型:Byte , Char , Short , Int , Long ,Float , Double , Boolean

这些类型都是类,跟Java的不同之处。

Scala并不刻意区分基本类型和引用类型。你可以对数字执行方法。

1.toString() // "1"
1.to(10) // Range(1-10)

Scala底层使用java.lang.String类表示自负床。不过,它通过StringOps类给字符串追加了上百种操作。

前者会被隐式转换为后者,再调用方法。

RichInt,RichDouble,RichChar分别为其原有类型提供了便捷方法。

BigIntBigDecimal 用于任意大小(但有穷)的数字。

Scala使用方法而不是强制类型转换,来做数值类型之间转换。

99.44.toInt
99.toChar
xx.toString

通常来说,可以用

a 方法 b 等价于  a.方法(b)

没有++ 和 – 运算符,需要使用 +=1或者-=1

能够对操作符进行重载 !@$&*这样的运算符

1.5 调用函数和方法

import scala.math._ 在Scala中,_字符是通配符,类似于Java中的*

使用scala前缀的包,可以省去scala前缀。

没有静态方法。单例对象。一个类有一个伴生对象,其方法就跟Java中的静态方法一样。例如:

BigInt.probablePrime(100,scala.util.Random)

Random是一个单例的随机数生成器对象,而该对象是在scala.util包中定义的

不带参数的Scala方法通常不使用圆括号。

"Hello"(4) // 将产出'o'

可以当作()的重载形式,原理是apply方法。

def apply(n: Int): Char  <=>  "Hello".apply(4)
BigInt("1234567789")

使用伴生对象的apply方法是Scala构造对象的常用方法。

1.7 Scaladoc

www.scala-lang.org/api

对应的类(C)或伴生对象(O)

第二章

所有语法 都可有值

2.1 条件表达式

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

例如:

if (x>0) "positive" else -1

上述表达式的类型是两个分支类型的公共超类型。 Any

又如:

if (x>0) 1

可能没有输出,但又需要有某种值。引入Unit类,写作() , 等价于

if (x>0) 1 else ()

可以当作void。

没有switch语句

解释器“近视”只看一行

if (x>0) { 1
} else if (x == 0) 0 else -1

在解释器复制粘贴代码

:paste
粘贴
CTRL+D

2.2 语句终止

if (n>0) {
    r = r*n
    n -= 1
}

这种方式较好

2.3 块表达式和赋值

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

val distance = { val dx = x-x0;val dy=y-y0; sqrt(dx*dx+dy*dy)}

赋值语句的返回类型是Unit类型。

2.4 输入和输出

print

println

printf()

readLine(“提示”)

2.5 循环

while (n>0) {
    
}
for (i <- 表达式)
表达式:to与util方法。
for ( i <- 0 util s.length)

没有for循环对应的

没有break continue

可以使用

  • 布尔类型控制变量

  • 使用嵌套函数——你可以从函数当中return

  • 使用 Breaks对象中的break方法

    import scala.util.control.Breaks._
    breakable {
        for (...) {
            if (...) break; //退出breakable块
        }
    }
    
    

2.6 高级for循环和for推导式

for ( i <- 1 to 3 ; j <- 1 to 3 if i != j)

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

for (i <- 1 to 10) yield i%3 //生成 Vector(1,2,0,1,2,0,1,2,0,1)

这类循环叫做for推导式。

for推导式生成的集合与它第一个生成器是类型兼容的。

for (c<- "Hello"; i <-0to1) yield (c+i).toChar // 等价于两个 for循环 遍历字母 每个字母自己和+1 "HIexxx"
for (i <- 0 to 1; c <- "Hello") yield (c+i).toChar

for { i <- 1 to 3
    from = 4 - i
    j <- from to 3}

2.7 函数

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

必须给出所有参数的类型。如果函数不是递归的,那么就不需要给出返回的类型。

scala可以通过= 右侧的表达式类型推断。

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

def fac(n: Int) = {
    var r =1 
    for (i <- 1 to n) r= r* i
    r
}

匿名函数中,如果使用return并不返回值给调用者。它跳出到包含它的带名函数中。

可以把return当作是函数版的break语句

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

def fac(n: Int): Int = if(n<=0) 1 else n * fac(n-1)

2.8 默认参数和带名参数

def decorate(str: String, left: String = "[" , right: String = "]") = 
	left + str + right

混用未命名的参数:未命名的参数在前面,带名的放在后面。带名参数还是很有用的。

decorate("Hello",right = "]<<<<")

2.9 变长参数

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

任意多的参数。

val s = sum(1,4,9,16,25)

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

如果sum函数被调用时传入的是单个参数,那么该参数必须是单个整数 。 : _*当作序列处理!

val s = sum(1 to 5: _*) // 将 1 to 5 当作参数序列处理
def recursiveSum(args: Int*): Int = {
    if ( args.length == 0) 0
    else args.head + recursiveSum(args.tail: _*)
}

在这里,序列的head是它的首个元素,而tail是所有其他元素的序列,这又是一个Seq,我们用:_*来将它转换成参数序列。

2.10 过程

Scala对于不返回值的函数有特殊的表示法。如果函数体包含在花括号当中但没有前面的=号,那么返回类型就是Unit。这样的函数被称为过程。过程不返回值,调用只是为了它的副作用。

def box(s: String) {
    var border = "-" * s.length + "--\n"
    printLn(border + "|" + s "|\n" + border)
}

显式声明Unit返回类型:其实全部都写等号好了,这样的话更明朗

def box(s: String): Unit = {
    ...
}

2.11 懒值

lazy val words = scala.io.Source.fromFile("/usr/share/dict/words").mkString

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

懒值对于开销较大的初始化语句而言十分有用。它还以应对其他初始化问题,那比如循环依赖。

懒数据结构的基础。

可以把懒值当作是介于val和def的中间状态

val words =  // 在words被定义时即被取值
lazy val words = // 在words被首次使用时取值
def words =  // 在每一次words被使用时取值

懒值并不是没有额外开销。我们每次访问懒值,都会有一个方法被调用,而这个方法将会以线程安全的方式检查该值是否已被初始化

2.12 异常

throw new IllegalArgumentException("x should not be negative")

抛出异常。当前运算被终止,运行时系统查找可以接受该异常的异常处理器。

对象必须是java.lang.Throwable的子类。不过,与java不同的是,scala没有受检异常——你不需要声明说函数或方法可能会抛出某种异常。

throw表达式有特殊的类型Nothing。如果一个分支的类型是Nothing,那么ifelse表达式的类型就是另一个分支的类型。

模式匹配方式

try {
    process()
} catch {
    case _:
    case ex:
}

更通用的异常应该放在具体异常后

不许哟啊使用捕获的异常对象,可以使用_来代替变量名

try/finally语句让你可以释放资源,不论有没有异常发生。例如:

var in = new URL
try {
    xx
} finally {
    
}

finally语句不论process函数是否抛出异常都会执行。

try/catch是处理异常,而try/finally语句在异常没有被处理时执行某种动作(通常是清理工作)。可以结合起来

try {...} catch {...} finally {...}

第三章 数组

  • 长度固定Array , 长度可能有变化ArrayBuffer

  • 提供初始值时不要使用new

  • 用()来访问元素

  • 用for(elem <- arr)来遍历元素

  • 用for(elem <- arr if …) … yield … 来将原数组转型为新数组

  • Scala数组和Java数组可以互操作;用ArrayBuffer,使用scala.collection.JavaConversions中的转换函数

3.1 定长数组

val nums = new Array[Int](10) // 10个整数的数组,所有元素初始化为0
val a = new Array[String](10) // 10个元素的字符串数组,所有元素初始化为null
val s = Array("Hello","World") // 长度为2的Array[String]
s(0) = "Goodbye" // 使用()访问元素

3.2 变长数组:数组缓冲

import scala.collection.mutable.ArrayBuffer
val b = ArrayBuffer[Int]() 
val c = new ArrayBuffer[Int]
b += 1 // +=尾部添加元素
b += (1,2,3,5)
b ++= Array(8,13,21) // ++=操作符追加任何集合
b.trimEnd(5) // 移除最后5个元素

b.insert(2,6) // 在下标 2 前加入元素 6
b.insert(2,7,8,9) // 在下标 2 前 加入任意多元素
b.remove(2) // 移除下标为 2 的元素
b.remove(2,3) // 连续移除下标为 2 的元素 3次

如果需要构建一个Array,但一开始不知道需要装多少个元素。在这种情况下,先构造一个数组缓冲,然后调用

b.toArray
反过来
b.toBuffer

3.3 遍历数组和数组缓冲

数组和向量语法相同。

0 until a.length
0 until (a.length,2) // 两跳
(0 until a.length).reverse

for (elem <- a)

3.4 数组转换

转换不会修改原始数组,而是产生一个全新的数组。

val a = Array(2,3,5,7,11)
val result = for (elem <- a) yield 2 * elem
val result = for (elem <- a if elem % 2 == 0) yield 2 * elem

var first = true 
val indexes = for( i<- 0 until a.length if first || a(i) >= 0) yield {
    if (a(i) < 0) first = true ; i
}

3.5 常用算法

Array(1,7,2,9).sum // ArrayBuffer同样适用
sum 必须是数值类型:要么整型,要么是浮点数或者BigInteger/BigDecimal

min和max输出数组或者数组缓冲中最小和最大的元素

sorted方法排序,但不会修改原始版本

val bSorted = b.sorted(_ < _) //b没有被改变

你还可以提供一个比较函数,不过你需要用sortWith方法

你可以直接对一个数组排序,但不能对数组缓冲排序:

scala.util.Sorting.quickSort(a)

对于min,max,quickSort方法,元素类型必须支持比较操作

如果要显示数组或者数组缓冲的内容,可以用mkString方法,它允许你指定元素之间的分隔符。

重载版本可以指定前缀和后缀。

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

a.toString // Array 无意义
b.toString // "ArrayBuffer(1,7,2,9)" 报告了类型,便于调试

3.6 解读Scaladoc

有一些文档里的解释。挺好的。!!!

3.7 多维数组

可以使用ofDim方法:

val matrix = Array.ofDim[Double](3,4) // 三行,四列
matrix(row)(column) = 42 // 要访问其中的元素,使用两对圆括号

可以创建不规则数组,每一行的长度不同

val r = new Array[Array[Int]](10)
for ( i <- 0 util r.length) 
	r(i) = new Array[Int](i + 1)

3.8 与Java的互操作

第四章 映射和元组

映射是键值对偶的集合。元组——n个对象的聚集,并不一定要相同的类型。对偶不过是一个n=2的元组。

4.1 构造Map

val scores = Map("Alice" -> 10,"Bob" -> 3,"Cindy" -> 8)

构造出一个不可变的Map[String,Int]

想要一个可变的映射可以用:

val scores = scala.collection.mutable.Map("Alice" -> 10)

如果想从一个空的映射开始,你需要选定一个映射实现并给出类型参数:

val scores = new scala.collection.mutable.HashMap[String,Int]

也可以这样创建

val scores = Map(("Alice",10),("Bob",3),("Cindy",8))

4.2 获取映射中的值

可以使用()表示法来查找某个键对应的值

val bobScore = scores("Bob")

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

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

val bobScore = if (scores.contains("Bob")) scores("Bob") else 0

快捷写法:

val bobScore = scores.getOrElse("Bob",0)

scores.get(键) 返回一个Option对象,要么是Some(键对应的值),要么是None

4.3 更新映射中的值

scores("Bob") = 10
scores("Fred") = 7  // 新增
scores += ("Bob" -> 10, "Fred" -> 7) // 新增多个
scores -= "Alice" //移除
val newScores = scores + ("Bob" -> 10, "Fred" -> 7) //更新过的新映射

要从不可变的移除,也可以使用:

scores = scores - "Alice"

不停地创建新映射效率不低,原因是老的和新的映射共享大部分结构。

4.4 迭代映射

遍历映射中所有的键值对偶:

for ((k,v)<- 映射) 处理k和v

scores.keySet // 类似Set("Bob","Cindy","Fred")
for( v <- scores.values) printfln(v) // 打印值

反转一个键和值:

for((k,v)<-映射) yield (v,k)

4.5 已排序映射

操作映射时,你需要选定一个实现,一个哈希表或平衡树。

默认情况下,是哈希表。

val scores = scala.collection.immutable.SortedMap("Alice"->10)

Scala没有可变的树形映射

如果需要可变的树形映射,可以使用java的TreeMap

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

val months = scala.collection.mutable.LinkedHashMap("January"->1 , "February"->2)

4.6 与Java的互操作

由TreeMap到scala的Map

import scala.collection.JavaConversions.mapAsScalaMap
val scores: scala.collection.mutable.Map[String,Int] = 
	new java.util.TreeMap[String,Int]

从scala映射传递给预期的java映射的方法,提供相反的隐式转换:

import scala.collection.JavaConversions.mapAsJavaMap
import java.awt.font.TextAttribute._ //引入下面的映射会用到的键
val attrs = Map(FAMILY -> "Serif",SIZE -> 12)
val font = new java.awt.Font(attrs) //该方法预期一个Java映射

4.7 元组

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

元组的值是通过将单个的值包含在圆括号中构成的。例如:

(1,3.14,"Fred") 类型为:
Tuple3[Int,Double,java.lang.String]

类型定义也可以写为:

(Int,Double,java.lang.String)

操作方法:

val t = (1,3.14,"Fred")
可以使用 _1 , _2 , _3 访问其组元
val second = t._2

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

._2  等价于 空格_2

通常,使用模式匹配来获取元组的组元,例如:

val (first,second,third) = t 
val (first,second,_) =t // 不需要的位置上使用_

元组可以用于函数返回不止一个值的情况。举例来说,StringOps的partition方法返回的是一堆字符串,分别包含了满足某个条件和不满足该条件的字符:

"New York".partition(_.isUpper) // 输出对偶(“NY”,“ew ork”)

4.8 拉链操作

使用元组的原因之一是把多个值绑在一起,以便他们能够被一起处理,这通常可以用zip方法来完成。

val symbols = Array("<","-",">")
val counts = Array(2,10,2)
val pairs = symbols.zip(counts)
Array(("<",2),("-",10),(">",2))
for((s,n) <- pairs) Console.print(s * n) // 会打印 <<---------->>

第5章 类

TIPS:

  • 类中必须初始化字段

  • 方法默认是公有的,私有的自己加private

  • 调用无参方法可以不写(),对于setter使用(),对于getter不用()

  • Scala对于字段都有相应的getter和setter方法。这两个方法是否公有,与声明一致。这是默认的。在任何时候也可以自己修改

  • 对于只读的字段,设为val即可

    • var foo : 自动生成setter与getter
    • val foo:自动生成getter
    • 由你来定义foo和foo_=方法
    • 由你来定义foo方法
  • private[this] var value = 0 更严格的访问限制。

  • 辅助Constructor:

    def this(name: String) : Unit = {
    	this() //调用主构造器
    	this.name = name
    }
    
    
  • 一个类如果没有显式定义主构造器,则自动拥有一个无参的主构造器即可。

  • 主构造器直接在类名之后:主构造器会执行类定义中的所有语句。私有的字段,公有的getter与setter方法。

    class Person(val name: String, val age: Int){
        ...//主构造器内容
    }
    
    
  • 主构造器的参数可以任意:例如 private var age: Intname: String, age: Int。如果不带val或var的话。

    class Person(name : String, age : Int) {
        def description = name + " is " +  age + " years old"
    }
    
    

    如果不带val或var的参数至少被一个方法所使用,它将被升格为字段。类似的字段等同于private[this] val字段的效果

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

  • 如果不喜欢主构造器,则使用辅助构造器,同时调用this就可以了!

  • TUDO 嵌套类。

5.1 简单类和无参方法

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

默认public,并不声明。

构造对象-》使用方法

val myCounter = new Counter // 或 new Counter()
myCounter.increment()
println(myCounter.current)

5.2 带getter和setter的属性

Scala对每个字段都提供一个get和set方法。

私有字段的get和set私有

公有字段的get和set公有

class Person {
    var age = 0
}
println(fred.age) //将调用方法 fred.age()
fred.age = 21 //将调用fred.age=(21)

在任何时候你都可以自己重新定义get和set方法。例如:

class Person {
    private var privateAge = 0
    
    def age = privateAge
    def age_=(newValue: Int) {
        if(newValue > privateAge) privateAge = newValue;
    }
}

5.3 只带get的属性

val time = new java.util.Date

5.4 对象私有字段

类似C++,但允许更加严格的访问限制:

private[this] var value = 0 //类似于 object.value这样的访问将不被允许

5.5 Bean属性

将Scala字段标注为@BeanProperty,这样的方法会自动生成。例如:

import scala.reflect.BeanProperty
class Person {
    @BeanProperty var name: String = _
}

会生成:

  • name: String
  • name_=(newValue: String): Unit
  • getName():String
  • setName(newValue: String): Unit
private[类名] val/var name  生成的方法依赖于具体实现,将访问权赋予外部类。并不经常用到

5.6 辅助构造器

主构造函数,辅助构造函数

  • 辅助构造器的名称为 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
    }
}

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

5.7 主构造器

主构造器不以this方法定义,而是与类定义交织在一起

主构造器的参数直接放置在类名之后

class Person(val name: String, val age: Int) {
    // (上面括号内)中的内容就是主构造器的参数
}

name和age都会成为Person的字段。

主构造器会执行类定义中的所有语句。例如在以下类中:

class Person(val name: String=“”, val age: Int = 0) {
    println("Just constructed another person")
    def description = name + " is " + age + " years old"
}

每当有对象被构造出来,上述代码就会被执行。

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

  • 如果不带val或var的参数至少被一个方法所使用,它将被升格为字段。例如:

    class Person(name: String, age: Int) {
        def description = name + " is " + age + " years old"
    }
    
    

    上述代码声明并初始化了不可比那字段name和age,而这两个字段都是对象私有的。

    等价于 private[this] val字段的效果

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

NOTE:

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

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

5.8 嵌套类

可以在任何语法结构中内嵌任何语法结构。在函数中定义函数,在类中定义类。

内部类在不同对象中是不同的

import scala.collection.mutable.ArrayBuffer
class Network {
    class Member(val name: String) {
        val contacts = new ArrayBuffer[Member]
    }
    private val members = new ArrayBuffer[Member]
    
    def join(name:String) = {
        val m = new Memeber(name)
        members += m
        m
    }
}

也就是说 相同名的内部类不能相互赋值

利用伴生对象解决

object Network {
    class Member(val name: String) {
        val contacts =  new ArrayBuffer[Member]
    }
}

class Network {
    private val members = new ArrayBuffer[Network.Member]
}

或者可以使用类型投影Netwokr#Member,其含义是“任何Network的Member”

class Network {
    class Member(val name: String) {
        val contacts = new ArrayBuffer[Network#Member]
    }
}

如果你只想在某些地方,而不是所有地方,利用这个细粒度的“每个对象有自己的内部类”的特性,可以考虑使用类型投影。

在内嵌类中,可以使用 外部类.this的方式来访问外部类的this引用,也可以建立一个指向该引用的别名:

class Network(val name: String) { outer =>
    class Member(val name: String) {
        ...
        def description = name + " inside " + outer.name
    }
}

outer变量指向Network.this。对这个变量,你可以用任何合法的名称。

第六章 对象

总结:TIPS

  • 为了弥补没有静态方法或静态字段而生。

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

  • 扩展类或特质的对象

  • Object(参数1,…,参数N):调用apply方法。在Object中定义:

    object Account {
        def apply(initialBalance: Double) =
        	new Account(newUniqueNumber(),initialBalance)
    }
    
  • 写main函数有两种方法:

    object Hello {
        def main(args: Array[String]) {
            println("Hello,World!")
        }
    }
    //(由于 DelayedInit,编译器对该特质有特殊的处理)P74
    object Hello extends App { // 扩展App特质,直接写具体的main函数即可。内置了args参数。
        println("Hello,World")
    }
    
  • 枚举不是关键字,是一个特质:Enumeration类。定义一个扩展Enumeration类的对象并以Value方法调用初始化枚举中的所有可选值。例如:

    object TrafficLightColor extends Enumeration {
        val Red,Yellow,Green = Value
    }
    
    每次调用Value方法都返回内部类的新实例,该内部类也叫做Value。
    val Red = Value(0,"Strop")
    val Yellow = Value(10) // 名称为"Yellow"
    val Green = Value("Go")
    
    

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

    调用方法:TrafficLightColor.Red、TrafficLightColor.Yellow、TrafficLightColor.Green。

    也可以直接import TrafficLgihtColor._

    注意枚举的类型是:TrafficLightColor.Value

6.1 单例对象

Scala没有静态 方法和对象,可以使用object来实现。

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

Account.newUniqueNumber()可以调用。

对象的构造器在第一次被使用的时候调用。

6.2 伴生对象

class Account {
    val id = Account.newUniqueNumber()
    private var balance = 0.0
    def deposit(amount: Double) { balance += amount; }
}

object Account { // 伴生对象
  	private var lastNumber = 0 
    private def newUniqueNumber() = { lastNumber += 1; lastNumber}
}

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

6.3 扩展类或特质的对象

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

一个有用的例子是给出可被共享的缺省对象。举例,可撤销动作的类:

abstract class UndoableAction(val description: String) {
    def undo(): Unit
    def redo(): Unit
}

object DoNothingAction extends UndoableAction("Do nothing") {
    override def undo() {}
    override def redo() {}
}

6.4 apply方法

apply方法返回的是伴生类的对象。

Array("Mary","had","a")

Array(100)与 new Array(100) 的区别。

前者调用apply方法,后者调用构造函数 this(100) 结果是Array[Nothing],包含了100个null元素

定义apply方法的示例:

class Account private (val id: Int, initialBalance: Double) {
    private var balance = initialBalance
}

object Account {
    def apply(initialBalance: Double) =
    	new Account(newUniqueNumber(),initialBalance)
}

这样就可以利用如下代码构造:

val acct = Account(1000.0)

6.5 应用程序对象

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

object Hello {
    def main(args: Array[String]) {
        println("Hello,World!")
    }
}

除了每次都提供自己的main方法外,你也可以扩展App特质,然后讲代码放入构造函数中

object Hello extends App {
    println("Hello,World!")
}
object Hello extends App {
    if (args.length > 0)
      println("Hello, " + args(0))
    else
      println("Hello, World!")
}

如果在调用该应用程序时设置了scala.time选项的话,程序退出时会显示逝去的时间。

scalac Hello.scala
scala -Dscala.time Hello Fred
Hello, Fred
[total 4ms]

App特质扩展自另一个特质DelayedInit,编译器对该特质有特殊处理。所有带有该特质的类,其初始化方法都会被挪到delayedInit方法中。App特质的main方法捕获到命令行参数,调用delayedInit方法,并且还可以根据要求打印出逝去的时间。

6.6 枚举

没有枚举类型。标准库提供了Enumeration助手类

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

object T extends Enumration {
    val Red,Yello,Green = Value
}

或者,你也可以向Value方法传入ID,名称,或两个参数都传:

val Red = Value(0,"Stop")
val Yellow = Value(10) // 名称为“Yellow”
val Green = Value("Go") // ID = 11

如果不指定,则ID在讲前一个枚举值的基础上加1,从0开始。缺省第二个字段名称 就是为 字段名

直接引入枚举值:

import T._

枚举的类型是T.Value

有人推荐增加一个类型别名:

object TrafficLightColor extends Enumeration {
    type TrafficLightColor = Value
    val Red,Yello,Green = Value
}

枚举的类型变成TrafficLightColor.TrafficLightColor,但仅当你使用import语句时,这样做才显得有意义。

import TrafficLightColor._
def doWhat(color: TrafficLightColor) = {
    if (color == Red) "stop"
    else if (color == Yellow) "hurry up"
    else "go"
}

枚举类型具有id方法,返回枚举值的ID,名称通过toString方法返回

对TrafficLightColor.values的调用输出所有枚举值的集:

for ( c<- TrafficLightColor.values) println(c.id+": "+ c)

最后,可以通过枚举的ID或名称来进行查找定位,以下两段代码都输出TrafficLightColor.Red对象:

TrafficLightColor(0)
TrafficLigthColor.withName("Red")

第7章 包和引入

7.1 包

package com{
    package horstmann {
        package impatient {
            class Employee
            ...
        }
    }
}
Employee.scala

与对象或类的定义不同,同一个包可以定义在多个文件当中。在Manager.scala中

package com {
    package horstmann {
        package impatient {
            class Manager
            ...
        }
    }
}

源文件目录与包无关

7.2 作用域规则

Scala的包和其他作用域一样的支持嵌套。你可以访问上层作用域中的名称。例如:

package com {
    package horstmann {
        object Utils {
            def ...
        }
        
        package impatient {
            class Employee {
                ...
                def giveRaise(rate: scala.Double) {
                    salary += Utils.percentOf(salary,rate)
                }
            }
        }
    }
}

这里有个陷阱,就是说 如果定义了同名的包,不能直接引用

解决的方法之一,使用绝对包名,以_root_开始,例如:

val subordinates = new _root_.scala.collection.mutable.ArrayBuffer[Employee]

7.3 串联式包语句

package com.horstmann.impatient {
    // com 和 com.horstmann的成员在这里不可见
    package people {
        class Person
        ...
    }
}

这样的包语句限定了可见的成员。现在com.horstmann.collection包不再能够以collection访问到了。

7.4 文件顶部标记法

你可以在文件顶部使用 package语句,不带花括号。例如:

package com.horstmann.impatient
package people
class Person
...

package com.horstmann.impatient {
    package people {
        class Person
        ...
        //直到文件末尾
    }
}

7.5 包对象

包可以包含类、对象、特质,但不能包含函数或变量的定义。由于JAVA虚拟机的局限。包对象解决

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

package com.horstmann.impatient

package object people {
    val defaultName = "John"
}

package people {
    class Person {
        var name = defaultName // 从包对象拿到的常量
    }
}

defaultName不需要加限定词,因为它位于同一个包内。在其他地方,可以用com.horstmann.impatient.people.defaultName访问到。

在幕后,包对象被编译成带有静态方法和字段的JVM类,名为package.class,位于相应的包下。

就是com.horstmann.impatient.people.package,其中有一个静态字段defaultName。package就是类名

可以把它放在,这样任何人想要对包增加函数或变量的话,都可以很容易找到对应的包对象

com/horstmann/impatient/people/package.scala 

7.6 包可见性

没有被声明为public、private、protected的类成员在包含该类的包中可见。在Scala中,你可以通过修饰符达到同样的效果。以下方法在它自己的包中可见:

package com.horstmann.impatient.people

class Person {
    private[people] def description = "A person with name" + name
    ...
}

可以将可见度延展到上层包:
private[impatient] def description = "A person with name" + name

7.7 引入

引入可以便于缩写。

引入某个包的全部成员:

import java.awt._

在scala中 *是合法的标识符。

import java.awt.Color._
val c1 = RED // Color.RED
val c2 = decode("#ff0000") // Color.decode

引入语句把它也带进了作用域

7.8 任何地方都可以声明引入

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

class Manager {
    import scala.collection.mutable._
    val subordinates = new ArrayBuffer[Employee]
}

7.9 重命名和隐藏方法

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

import java.awt.{Color,Font}

选取器允许你重命名宣导的成员:

import java.util.{HashMap -> JavaHashMap}
import scala.collection.mutable._

选取器HashMap => _将隐藏某个成员而不是重命名它。这仅在你需要引入其他成员时有用:

import java.util.{HashMap => _, _}
import scala.collection.mutable._

现在,HashMap无二义指向。

7.10 隐式引入

每个Scala程序都隐式地以如下代码开始:

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

java.lang总是被引入。scala包也会被引入,不过方式有点特殊。不像所有其他引入,这个引入被允许可以覆盖之前的引入。举例来说,scala.StringBuilder会覆盖java.lang.StringBuilder而不是冲突。

Predef对象的引入。他包含了相当多有用的函数。

由于scala包默认被引入,对于那些以scala开头的包,不用写这个前缀。

collection.mutable.HashMap

第8章 继承

8.1 扩展类

extends final

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

8.2 重写方法

重写一个非抽象方法必须使用override修饰符,例如:

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

在Scala中调用超类的方法和Java完全一样,使用super关键字:

public class Employee extends Person {
    ...
    override def toString = super.toString + "[salary=" + salary + "]"
}

8.3 类型检查和转换

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

if(p.isInstanceOf[Employee]) {
    val s = p.asInstanceOf[Employee] // s的类型为Employee
}

如果p指向的是Employee类及其子类对象,isInstanceOf[Employee]会返回成功

如果p是Null,则返回false。

如果p不是一个Employee,那么抛出异常

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

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

classOf方法定义在scala.Predef对象中,会被自动引入

模式匹配比类型转换更好:

p match {
    case s: Emolyee => ... //将s作为Employee处理
    case _ => //p不是Employee
}

8.4 受保护字段和方法

可以将字段或方法声明为protected。这样的成员可以被任何子类访问,但不能从其他位置看到。

protected的成员对于类所属的包而言,是不可见的。

scala还提供了一个protected[this]的变体,将访问权限定在当前的对象。

8.5 超类的构造

辅助构造器永远都不可能直接调用超类的构造器

子类的辅助构造器最终都会调用主构造器。只有主构造器可以调用超类的构造器

class Employee(name: String,age: Int,val salary: Double) extends Person(name,age)

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

8.6 重写字段

子类可以覆盖基类的 成员字段

class Person(val name: String) {
    override def toString = getClass.getNAME + "[name=" + name + "]"
}

class SecretAgent(codename: String) extends Person(codename) {
    override val name = "secret" 
    override val toString = "secret"
}

更常见的例子是用val重写抽象的def,就像 :

abstract class Person {
    def id: Int
}
class Student(override val id: Int) extends Person // 学生id通过构造器输入

NOTE:

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

8.7 匿名子类

可以通过包含带有定义或重写的代码块的方式创建一个匿名的子类,比如:

val alien = new Person("Fred") {
    def greeting = "Greetings. Earthling!"
}

会创建一个结构类型的对象(参见18章)。

该类型标记为Person{def greeting: String}。你可以用这个类型作为参数类型的定义:

def meet(p: Person{def greeting: String}) {
    println(p.name + "says: " + p.greeting)
}

8.8 抽象类

abstract关键字来标记不能被实例化的类,通常这是因为它的某个或某几个方法没有完整定义。例如:

abstract class Person(val name:String) {
    def id: Int //没有方法体,这是一个抽象方法
}

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

class Employee(name: String) extends Person(name) {
    def id = name.hashCode // 不需要override关键字
 }

8.9 抽象字段

抽象字段就一个没有初始值的字段。例如:

abstract class Person(val name: String) {
    val id: Int // 没有初始化,带有抽象的get方法的抽象字段
    var name: String // 另一个抽象字段,带有抽象的get和set方法
}

该类为id和name定义了抽象的get方法,为name字段定义了抽象的set方法。

具体的子类必须提供具体的字段,例如:

class Employee(val id: Int) extends Person { //子类有具体的id树形
    var name = “” //和具体的name属性
}

和方法一样,在子类中重写超累中的抽象字段时,不需要override关键字。

匿名类型定制抽象字段:

val fred = new Person {
    val id = 1729
    var name = "Fred"
}

8.10 构造顺序和提前定义

子类重写val并且在 超类的构造器中使用该值的话,而 superclass 的构造器 先于 子类的构造器运行。过程:

  • Ant的构造器在做它自己的构造之前,调用 Creature的构造器
  • Creature的构造器将它的range字段设为10
  • Creature的构造器为了初始化env数组,调用range()取值器
  • 该方法被重写以输出(还未初始化的)Ant类的range字段值
  • range方法返回0。(这是对象被分配空间时所有整形字段的初始值)。
  • env被设为长度为0 的数组
  • Ant构造器继续执行,将其range字段设为2

虽然range字段看上去可能是10或者2 ,但env被射程了偿付为0 的数组。这里的教训是你在构造器内不应该依赖val的值。

解决方法:

  • 将val声明为 final 。 这样很安全但不灵活
  • 在超类中将 val 声明为 lazy(参见第二章) 。 这样安全但不高效
  • 在子类中使用提前定义语法

提前定义语法让你可以在超类的构造器执行之前初始化子类的val字段。语法:

class Ant extends {
    override val range = 2
} with Creature

提前定义的等号右侧只能引用之前已有的提前定义,而不能使用类中的其他字段

8.11 Scala继承层次

基本类型以及 Unit类型,都扩展自 AnyVal。

所有其他的类都是AnyRef的子类,AnyRef相当于 JAVA中的Object类的同义词。

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

Any类定义了isInstanceOfasInstanceOf方法,以及用于相等性判断和哈希码的方法。

AnyVal并没有追加任何方法。它只是所有值类型的一个标记。

AnyRef类追加了来自Object类的监视方法wait和notify/notifyAll。同时提供了一个带函数参数的方法synchronized。这个方法等同于java中的synchronize块。例如:

account.synchronized {account.balance += amount }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-owqhwI5E-1662286997133)(C:\Users\johnqyang\AppData\Roaming\Typora\typora-user-images\image-20210717204243446.png)]

建议:不适用wait、notify和synchronized,除非有充分的理由不使用更高层级的并发结构

所有的Scala类都实现了 ScalaObject这个标记接口,这个接口没有定义任何方法。

在继承层级的另一端是Nothing和Null类型。

Null类型的唯一实例时null值。你可以将null复制给任何的 Ref ,但不能复制给 Val类型的变量。举例就是,Int类型不能复制Null

Nothing类型没有实例。它对于泛型结构更常用。空列表Nil类型时List[Nothing],它时List[T]的子类,T可以时任何类。

在Scala中,void由Unit类型表示,该类型只有一个值,那就是()。注意Unit并不是任何其他类型的超类型。但是,编译器依然允许任何值被替换为()。

8.12 对象相等性

在Scala中,AnyRef的eq方法检查两个引用是否指向同一个对象。AnyRef的equals方法调用eq。当你实现类的时候,应该考虑重写equals方法,以提供一个自然的、与你的实际情况相称的相等性判断。

举例来说,如果你定义class Item(val description: String,val price: Double),你认为相同描述和价格时候这两个是相等的。equals的定义可以是:

final override def equals(other: Any) = {
    val that = other.asInstanceOf[Item]
    if (that == null) false
    else description == that.description && price == that.price
}

注意:请确保定义的equals方法参数类型为 Any!

当你定义equals时,记得同时也定义hashCode。在计算哈希码时,只应该使用那些你用来做相等性判断的字段。

final override def hashCode = 13 * description.hashCode + 17 * price.hashCode

通常不直接调用equals,只需要用 == 操作符就好。对于引用类型,它会在做完必要的null检查后调用equals方法。

第9章 文件和正则表达式

9.1 读取行

读取文件所有行:

import scala.io.Source
val source = Source.fromFile("myfile.txt","UTF-8")
val lineIterator = source.getLines // 结果是个迭代器
for ( l <- lineIterator) // 逐条处理这些行
val lines = source.getLines.toArray //或者对迭代器以用toArray或toBuffer方法
val contents = source.mkString //整个文件读取为一个自负床

使用完Source对象,记得调用close

9.2 读取字符

直接把Source对象当作迭代器,因为Source类扩展自Iterator[Char]:

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

查看某个字符但又不处理掉它,调用source对象的buffered方法。这样可以用 head方法查看下一个字符,但同时并不把它当作是已处理的字符。

val source = Source.fromFile("myfile.txt","UTF-8")
val iter = source.buffered
while (iter.hasNext) {
    if (iter.head 是符合预期的) 
    	处理 iter.next
   	else
    	...
}
source.close()

或者,如果文件不大,可以读取成一个字符串处理

9.3 读取词法单元和数字

快而脏的方式读取源文件中所有以空格隔开的词法单元:

val tokens = source.mkString.split("\\S+")

将字符串转换为数字,可以用toInt或toDouble方法。举例:

val numbers = for( w <- tokens) yield w.toDouble
val numbers = tokens.map(_.toDouble)

从控制台读取数字:

val age = readInt() // readDouble readLong

假定输入仅由单个数字,且前后都没有空格。否则会报NumberFormatException

9.4 从URL或其他源读取

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

val source1 = Sourc.fromURL("http://horstmann.com","UTF-8")
val source2 = Source.fromString("Hello World")
val source3 = Source.stdin

9.5 读取二进制文件

Scala没有提供读取二进制文件的方法。你需要使用Java类库。以下是如何将文件读取成字节数组:

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

9.6 写入文本文件

Scala没有内建的对写入文件的支持。要写入文本文件,可以使用java.io.PrintWriter,例如:

val out = new PrintWriter("numbers.txt")
for (i <- 1 to 100) out.println(i)
out.close()

所有的逻辑都像我们预期的那样,除了printf方法除外。当你传递数字给printf时,编译器会抱怨说你需要将它转换长AnyRef:

out.printf("6d %10.2f",quantity.asInstanceOf[AnyRef],price.asInstanceOf[AnyRef])

为避免这个麻烦,可以使用string类的format方法:

out.print("%6d".format(quantity))

9.7 访问目录

没有正式的访问某个目录的所有文件,或者递归地遍历所有目录的类。

遍历某个目录下的所有子目录的函数:

import java.io.File
def subdirs(dir: File): Iterator[File] = {
    val children = dir.listFiles.filter(_.isDirectory)
    children.toInterator ++ children.toIterator.flatMap(subdirs _)
}

利用这个函数,你可以像这样访问所有子目录:

for (d <- subdirs(dir)) 处理d

或者,java 7,可以使用 java.nio.file.Files类的walkFileTree方法 TUDO

9.8 序列化

声明一个可被序列化的类。

@SerialVersionUID(42L) class Person extends Serializable

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

如果你可以接受缺省的ID,也可以略去@注解

9.9 进程控制

scala.sys.process包提供了用于shell程序交互的工具。你可以用scala编写shell脚本,利用Scal提供的所有能力。

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

这样的结果是ls -al .. 命令被执行,显示上层目录的所有文件。执行结果被打印到标准输出。

sys.process包包含了一个从字符串到ProcessBuilder对象的隐式转换。!操作符执行的就是这个ProcessBuilder对象。

!操作符返回的结果是被执行程序的返回值:程序成功执行的话就是0,否则就是显示错误的非0值。

如果你使用!!而不是!的话,输出会以字符串的形式返回:

val result = "ls -al .." !!

你还可以将一个程序的输出以管道形式作为输入传送到另一个程序,用#|操作符:

"ls -al .." #| "grep sec" !

输出重定向到文件,使用#>操作符:

"ls -al .." #> new File("output.txt") !

要追加到文件末尾而不是覆盖,使用#>>

"ls -al .." #>> new File("output.txt") !

要把某个文件作为输入,使用#<

"grep sec" #< new File("output.txt") !

从URL重定向输入:

"grep Scala" #< new URL("http://horstmann.com/index.html") !

你可以将进程结合一起使用,比如p #&& q(如果p成功,执行p),以及p #|| q(如果p不成功,执行q)。不过Scala可比shell的流转控制强太多了,可以直接使用scala的流转控制。

如果你需要再不同的目录下运行进程,或者使用不同的环境变量,用Process对象的apply方法来构造ProcessBuilder,给出命令和起使目录,以及一串(名称,值)对偶来设置环境变量:

val p =Process(cmd, new File(dirName), ("LANG","en_US"))

然后调用:

"echo 42" #| p !

9.10 正则表达式

scala.util.matching.Regex类实现,构造一个Regex对象,用String类的r方法即可:

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

如果正则表达式包含反斜杠或引号的话,那么最好使用“原始”字符串语法"""..."""

val wsnumwsPattern = """\s+[0-9]+\s+""".r

findAllIn方法返回遍历所有匹配项的迭代器。你可以在for循环中使用它:

for (matchingString <- numPattern.findAllIn("99 bottles, 98 bottles"))

或者将迭代器转换成数组:

val matches = numPattern.findAllIn("99 bottles").toArray

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

val m1 = wsnumwsPattern.findFirstIn("99 bottles") 

要检查是否某个字符串的开始部分能匹配,可用 findPrefixOf :

numPattern.findPrefixOf("99 bottles") // Some(99)

替换首个匹配项,或全部匹配项:

numPattern.replaceFirstIn("99 bottles","XX")
numPattern,replaceAllIn("99 88","XX") //"XX XX"

9.11 正则表达式组

分组可以让我们方便地获取正则表达式的子表达式。在你想要提取的子表达式两侧加上圆括号,例如:

val numitemPattern = "([0-9]+) ([a-z]+)".r

要匹配组,可以把正则表达式对象当作"提取器"(参见第14章)使用,就像这样:

val numitemPattern(num,item) = "99 bottles" // 将num设为“99”,item设为“bottles”

如果你想要从多个匹配项中提取分组内容,可以像这样使用for语句:

for(numitermPattern(num,item) <- numitemPattern.findAllIn("99 bottles, 98 bottles"))
	处理num和item

第10章 特质

一个类扩展自一个或多个特质,以便使用这些特质提供的服务。特质可能会要求使用它的类支持某个特定的特性。

  • 特质可以既有抽象方法也有具体方法

  • 重写特质的抽象方法不需要给出override关键字

  • 多特质:class ConsoleLogger extends Logger with Coneable with Serializable。多with

  • 类中直接使用特质,直接调用函数即可

  • 带有特质的对象。可以混入一个特质。新写一个特质继承所混入的特质。在创建对象的时候。可以使用val acct = new SavingsAccount with ConsoleLogger

  • 可以混入多个特质,按序的。最先执行最右侧的特质。

  • 在特质中重写抽象方法:

    abstract override def log(msg: String) { //需要加上abstract override
        super.log(new java.util.Date() + " " + msg)
    }
    
    
  • 特质中也可以有字段。如果有初始值,那么就是具体的,如果没有初始值,那么就是具体的。字段是直接加到子类之中的。

  • 特质中未被初始化的字段在具体的子类中必须被重写。规定了必须要什么。

  • 初始化特质中的字段:使用懒值。

  • 混入超类的特质。在混入该特质的时候,子类的超类也就变成了特质原本的超类。如果还想让子类继承某个超类,那么要求这个超类是特质超类的子类

10.1 为什么没有多重继承

特质可以同时拥有抽象方法和具体方法,而类可以实现多个特质。存在冲突。

10.2 当做接口使用的特质

trait Logger {
    def log(msg: String) //抽象方法
}

注意,不需要声明为abstract。特质中未被实现的方法默认就是抽象的。

子类给出实现:

class ConsoleLogger extends Logger { // 用extends
    def log(msg: String) {println(msg)} //不需要写override
}

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

class ConsoleLogger extends Logger with Cloneable with Serializable

所有Java接口都可以作为Scala特质使用

Scala类只能有一个超类,但可以有任意数量的特质。

10.3 带有具体实现的特质

不需要一定是抽象的。

trait ConsoleLogger {
    def log(msg: String) {println(msg)}
}

使用这个特质的示例:

class SavingsAccount extends Account with ConsoleLogger {
    def withdraw(amoun: Double) {
        if(amount > balance) log("INsufficient funds")
        else balance -= amount
    }
}

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

10.4 带有特质的对象

在构造单个对象时,你可以为它添加特质。作为示例,用标准Scala库中的Logged特质:

trait Logged {
    def log(msg: String) {}
}

class SavingsAccount extends Account with Logged {
    def withdraw(amount: Double) {
        if(amount > balance) log("Insufficient funds")
        else ...
    }
    ...
}

现在,什么都不会被记录到日志,看上去毫无意义。但是你可以在构造具体对象的时候”混入“一个更好的 日志记录器的实现。

trait ConsoleLogger extends Logged {
    override def log(msg: String) {println(msg)}
}

你可以在构造对象的时候加入这个特质:

val acct = new SavingsAccount with ConsoleLogger

当我们在acct对象上嗲用log方法时,ConsoleLogger特质的log方法就会被执行。

当然了,另一个对象可以加入不同的而特质:

val acct2 = new SavingsAccount with FileLogger

10.5 叠加在一起的特质

你可以为类或对象添加多个互相调用的特质,从最后一个开始。这对于需要分阶段加工处理某个值的场景很有用。

给所有日志消息添加时间戳:

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

截断过于冗长的日志消息:

trait ShortLogger extends Logged {
    val maxLength = 15 
    override def log(msg: String) {
        super.log(
        if (msg.length <= maxLength) msg else msg.substring(0,maxLength-3) + "...")
    }
}

叠加在一起的顺序:从最后一个开始第一个执行,依次往上:

val acct1 = new SavingsAccount with ConsoleLogger with TimestampLogger with ShortLogger
val acct2 = new SavingsAccount wiht CinsoleLogger with ShortLogger with TimestampLogger
// acct1 => Sun Feb 06 17:45:45 ICT 2011 Insufficient...
// acct2 => Sun Feb 06 1...

如果你需要控制具体是哪一个特质的方法被调用,则可以在方括号里给出名称:

super[ConsoleLogger].log(...)

这里给出的类型必须是直接超类型;你无法使用继承层级中更远的特质或类。

10.6 在特质中重写抽象方法

trait Logger {
    def log(msg: String) 
}

trait TimestampLogger extends Logger {
    override def log(msg: String) {
        super.log(new java.util.Date() + " " + msg) // super.log没有定义 错误
    }
}

编译器将super.log调用标记为错误。

根据正常的继承规则,这个调用永远都是错误的,Logger.log没有实现。但实际上,就像你在前一节看到的,我们没法知道哪个log方法最终被调用——这取决于特质被混入的顺序。

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

Scala认为TimestampLogger依旧是抽象的——它需要混入一个具体的log方法。因此你必须给方法打上abstract关键字以及override关键字。

10.7 当作富接口使用的特质

特质可以包含大量工具方法,而这些工具方法可以依赖于一些抽象方法来实现。例如Scala的Iterator特质就利用抽象的next和hasNext定义了几十个方法。

trait Logger {
    def log(msg: String)
    def info(msg: String) { log("INFO: " + msg)}
    def warn(msg: String) { log("WARN: "+ msg)}
    def severe(msg: String) { log("SEVERE: " + msg)}
}

注意我们是如何把抽象方法和具体方法结合起来的。

使用Logger特质的类就可以任意调用这些日志消息方法了,例如:

class SavingsAccount extends Account with Logger {
    def withdraw(amount: Double) {
        if(amount > balance) severe("Insufficient funds")
        else ...
    }
    ...
    override def log(msg: String) { println(msg) }
}

10.8 特质中的具体字段

特质中的字段可以是具体的,也可以是抽象的。如果你给出了初始值,那么字段就是具体的。

trait ShortLogger extends Logged {
    val maxLength = 15
    ...
}

混入该特质的类自动获得 一个maxLength字段。这些字段不是被继承的;他们只是简单地被加到了子类当中。

class SavingsAccount extends Account with ConsoleLogger with ShortLogger {
    var interest = 0.0
    def withdraw(amount: Double) {
        if(amount > balance) log("Insufficient funds")
        else ...
    }
}

假设:

class Account {
    var balance = 0.0 
}

SavingsAccount继承了这个字段,它由所有超类的字段,以及子类中定义的字段构成。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dukLddvd-1662286997134)(C:\Users\johnqyang\AppData\Roaming\Typora\typora-user-images\image-20210718200828218.png)]

来自特质的字段被放置在子类中

在JVM中,一个类只能扩展一个超类,因此来自特质的字段不能以相同的方式继承。

10.9 特质中的抽象字段

特质中未被初始化的字段在具体的子类中必须被重写。

不需要写override

10.10 特质构造顺序

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

trait FileLogger extends Logger {
    val out =new PrintWriter("app.log") // 特质构造器的一部分
    out.println("# " + new Date().toString)  // 特质构造器的一部分
    
    def log(msg: String) { out.println(msg);out.flush()}
}

这些语句在任何混入该特质的对象在构造时都会被执行。

构造器以如下顺序执行:

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

10.11 初始化特质中的字段

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

这个局限对于那些需要某种定制才有用的特质会是个问题,例如:

val acct = new SavingsAccount wiht FileLogger("myapp.log") //error
trait FileLogger with Logger {
    val filename: String
    val out = new PrintStream(filename) // trait先于子类构造,会空指针异常
    def log(msg: String) { out.println(msg); out.flush()}
}

解决方法:

val acct = new {
    val filename ="myapp.log"
} with SavingsAccount with FileLogger

如果要在类中做同样的事情,语法:

class SavingsAccount extends {
    val filename = "savings.log"
} with Account with FileLogger {
    ... // SavingsAccount的实现
}

另一个解决方法是在FileLogger构造器中使用懒值,就像这样:

trait FileLogger extends Logger {
    val filename: String
    lazy val out =new PrintStream(filename)
    def log(msg: String) { out.println(msg) } //不需要写override
}

如此一来,out字段将在初次被使用时才会被初始化。而在那个时候,在filename字段应该已经被设置好值了。不过,由于懒值子啊每次使用前都会检查是否已经初始化,就是不那么高效

10.12 扩展类的特性

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

trait LoggedException extends Exception with Logged {
    def log() {log(getMessage)}
}

创建一个混入该特质的类:

class UnhappyException extends LoggedException {
    override def getMessage() = "arggh!"
}

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

如果我们的类已经扩展了另一个类怎么办?只要是特质的超类的一个子类就没关系

class UnhappyException extends IOException with LoggerdException

不过,如果我们的类扩展自一个不相关的类,那么就不可能混入这个特质了。

class UnhappyFrame extends JFrame with LoggedException //错误:不相关的超类

10.13 自身类型

当特质扩展类时,编译器能够确保的一件事就是混入该特质的类都认为这个类作为超类。Scala还有另一套机制可以保证:自身类型(selftype)

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

this: 类型 =>

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

trait LoggedException extends Logged {
    this: Exception =>
    	def log() {log(getMessage())}
}

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

在特质的方法中,我们可以调用该自身类型的任何方法。举例来说,log方法中的getMessage()调用就是合法的,因为我们知道this必定是一个Exception。

如果你想把这个特质混入一个不符合自身类型要求的类,就会报错。

带有自身类型的特质和带有超类型的特质很相似。两种情况都能确保混入该特质的类能够使用某个特定类型的特性。

在某些情况下自身类型这种写法比超类型版的特质更灵活。自身类型可以解决特质间的循环依赖。如果你有两个彼此需要的特质时循环依赖就会产生。

自身类型也同样可以处理结构类型——这种类型只给出类必须拥有的方法,而不是类的名称。以下是使用结构类型的LoggedException定义:

trait LoggedException extends Logged {
    this: { def getMessage(): String} =>
     def log() {log(getMessage())}
}

这个特质可以被混入任何拥有getMessage方法的类

10.14 背后发生了什么

将类和特质翻译成 JVM的类和接口的对应关系。

第11章 操作符

11.1 标识符

可以在反引号中包含几乎任何字符序列。例如:

val `val` = 42

11.2 中置操作符

a 标识符 b

标识符代表一个带有两个参数的方法(一个隐式的参数和一个显式的参数)

举例:

class Fraction(n: Int, d: Int) {
    private int num = 
    private int den = 
    ...
    def *(other: Fraction) = new Fraction(num * other.num, den * other.den) //定义
}

11.3 一元操作符

a 标识符 等价于 a.标识符()

如下+-!~可以作为前置操作符,被转换成对应的unary_操作符的方法调用。

-a 等价于 a.unary_- 

11.4 赋值操作符

a 操作符= b

11.5 优先级

加括号吧,别记了

11.6 结合性

所有操作符都是左结合的,除了:

  • 以冒号结尾的操作符
  • 赋值操作符

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

1 :: 2 :: Nil 等价于 Nil.::(2) 不全 

11.7 apply和update方法

f(arg1,arg2,...) = value 等价于 f.update(arg1,arg2,...,value)

这个机制被用于数组和映射,例如:

val scores = new scala.collection.mutable.HashMap[String,Int]
scores("Bob") = 100

apply方法同样被经常用在伴生对象中,用来构造对象而不用显示地使用new。假定有一个Fraction类:

class Fraction(n: Int,d: Int) {
    ...
}

object Fraction {
    def apply(n: Int,d : Int) =  new Fraction(n,d)
}

因为有了这个apply方法,我们可以用Fraction(3,4)来构造一个分数,而不用new。

11.8 提取器

提取器就是一个带有unapply方法的对象。你可以把unapply方法当作是伴生对象中apply方法的反向操作。unapply方法接受一个对象,然后从中提取值,通常这些值都是当初用来构造该对象的值

var Fraction(a,b) = Fraction(3,4) * Fraction(2,5)

object Fraction {
    def unapply(input: Fraction) = 
    	if (input.den == 0) None else Some((input.num,input,den))
}

返回的是 Option[(Int,int)]

使用举例:

val author = "AA BB"
val Name(first,last) = author //unapply

11.9 带单个参数或无参数的提取器

没有只带一个组件的元组。如果unapply方法要提取单值,则它应该返回一个目标类型的Option。例如:

object Number {
    def unapply(input: String): Option[Int] = 
    	try {
            Some(Integer.parseInt(input.trim))
        } catch {
            case ex: NumberFormatException => None
        }
}

用这个提取器,你可以从字符串中提取数字:

val Number(n) = "1720"

提取器也可以只是测试而并不真的将值去除。这样的话,unapply方法返回的是Boolean。例如:

object IsCompound {
    def unapply(input: String) = input.contains(" ")
}

你可以用这个提取器给模式增加一个测试,例如:

author match {
    case Name(first,last @ IsCompound()) => ...
    case Name(first,last) => ...
}

11.10 unapplySeq方法

提取任何长度的值的序列,使用unapplySeq方法。返回Option[Seq[A]],其中A是被提取的值的类型。

object Name {
    def unapplySeq(input: String): Option[Seq[String]] = 
    	if (input.trim == "") None else Some(input.trim.split("\\s+"))
}

这样以来可以匹配任意数量的变量了:

author match {
    case Name(first,last) => ...
    case Name(first,middle,last) => ..
    case Name(fiset,"van","der",last) => ...
}

第12章 高阶函数

12.1 作为值的函数

函数就和数字一样。可以在变量中存放函数:

import scala.math._
val num = 3.14
val fun = ceil _

ceil函数后的 _ 意味着你确实指的是这个函数,而不是碰巧忘记了给它传递参数

能对函数做的两件事:

  • 调用它
  • 传递它,存放在变量中,或者作为参数传递给另一个函数
fun(num) // 4.0

fun是一个包含函数的变量,而不是一个固定的函数。

传递给另一个函数:

Array(3.14,1.42,2.0).map(fun)

map方法接受一个函数参数,将它应用到数组中的所有值,然后返回结果的数组。

12.2 匿名函数

你不需要给每一个函数命名,正如你不需要给每个数字命名一样。以下是一个匿名函数:

(x: Double) => 3*x

存放在变量

val triple = (x: Double) => 3 * x 等价于
def tripe(x: Double) = 3 * x
Array(3.14,1.42,2.0).map((x: Double)=> 3*x)

12.3 带函数参数的函数

def valueAtOneQuartr(f: (Double) => Double) = f(0.25)

例如:

valueAtOneQuarter(ceil _) // 1.0
valueAtOneQuarter(sqrt _) // 0.5

接受函数参数的函数,称为高阶函数

高阶函数产出另一个函数:

def mulBy(factor: Double) = (x: Double) => factor * x

val quintuple = mulBy(5)
quintupe(20) // 100

12.4 参数(类型)推断

可以简写:

valueAtOneQuarter((x) => 3*x)

只有一个参数的函数,可以再略去():

valueAtOneQuarter(x => 3*x)

如果参数在=>右侧只出现一次,你可以用_替换掉它:

valueAtOneQuarter(3 * _)

一个将某值乘以3的函数

请注意这些简写方式仅在参数类型已知的情况下有效:

val fun = 3 * _ //error
val fun = 3 * (_: Double) // correct
val fun: (Double) => Double = 3 * _  // fun的类型是(Double)=>Double , _的类型就是 Double 

12.5 一些有用的高阶函数(最大差别:用函数定义另一个函数)

(1 to 9).map("*" * _).foreach(println _)
filter方法输出所有匹配某个特定条件的元素。
(1 to 9).filter(_ % 2 == 0) 

reduceLeft方法接受一个二元的函数——即一个带有两个参数的函数——并将它应用到序列中的所有元素,从左到右。例如:

(1 to 9).reduceLeft(_ * _) // 等价于 1*2*3*4*5*6*7*8*9

做排序:

"Mary has a little lamb".split(" ").sortWith(_.length < _.length)
//输出长度递增排序的数组: Array("a","had","Mary","lamb","little")

12.6 闭包

可以在任何作用域内定义函数:包、类甚至是另一个函数或方法。

在函数体内,你可以访问到相应作用域内的任何变量。你的函数可以在变量不再处于作用域内时被调用。

例如:

def mulBy(factor: Double) = (x: Double) => factor * x
val tripe = mulBy(3)
val half = mulBy(0.5)
println(triple(14)) + “ ” + half(14)

每一个返回的函数都有自己的factor设置。

这样一个函数都有自己的factor设置。

这样的一个函数被称为闭包。闭包由代码和代码用到的任何非局部变量定义构成。

这些函数实际上是以类的对象方式实现 的,该类有一个实例变量factor和一个包含了函数体的apply方法。

12.7 SAM转换

告诉另一个函数做某件事时。隐式转换。 TUDO。

12.8 柯里化

指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数作为参数的函数。

def mul(x: Int, y: Int) = x * y
def mulOneAtATime(x: Int) = (y: Int) => x*y

要计算两个乘积,调用:

mulOneAtATime(6)(7)

支持简写定义柯里化函数:

def mulOneAtATime(x: Int)(y: Int) = x * y

典型例子:

val a = Array("Hello","World")
val b = Array("hello","world")
a.corresponds(b)(_.equalsIgnoreCase(_))
def corresponds[B](that: Seq[B])(p: (A,B) => Boolean): Boolean

先确定了B,再确定A

12.9 控制抽象

可以将一系列语句归组成不带参数也没有返回值的函数。举例:

def runInThread(block: ()=> Unit) {
    new Thread {
        override def run() {block()}
    }.start()
}

使用:

runInThread{ () => println("Hi");Threads.sleep(1000);println("Bye")}

要想在调用中省去()=> ,可以使用换名调用表示法:在参数声明和调用该函数参数的地方省略(),但保留=>:

def runInThread(block: => Unit) {
    new Thread {
        override def run() { block }
    }.strat()
}

使用变成:

runInthread{ println("Hi"); Threads.sleep(1000);println("Bye")}

Scala可以构建控制抽象:看上去像是变成语言的关键字的函数。完全可以定义一个像while函数那样的,比如定义一个until函数:

def until(condition: => Boolean)(block: => Unit) {
    if (!condition) {
        block
        until(conditon)(block)
    }
}
var x = 10
until(x == 0) {
    x -= 1
    println(x)
}

这样的函数参数有一个专业术语叫做换名调用参数。和一个常规的参数不同,函数在被调用时,参数表达式不会被求值。

12.10 return表达式

不需要使用return语句来返回函数值。函数的返回值就是函数体的值。

不过,你可以用return来从一个匿名函数中返回值给包含整个匿名函数的带名函数。这对于控制抽象是很有用的。

例如:

def indexOf(str: String, ch: Char): Int = {
    var i = 0
    until (i == str.length) {
        if(str(i) == ch) return i
        i += 1
    }
    return -1
}

直接终止包含它的带名函数。

如果你要在带名函数中使用return的话,则需要给出其返回类型。举例来说,就是在上述的indexOf函数中,编译器没法推断出它会返回Int。

控制流程的实现依赖一个在匿名函数的return表达式中抛出的特殊异常,该异常从until函数传出,并被indexOf函数捕获。

注意:如果异常在被送往带名函数值前,在一个try代码块中被捕获掉了,那么相应的值就不会被返回。

第13章 集合

13.1 主要的集合特质

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-45IqjpMr-1662286997135)(C:\Users\johnqyang\AppData\Roaming\Typora\typora-user-images\image-20210719094927962.png)]

Iterable指的是那些能生成用来访问集合中所有元素的Iterator的集合:

val coll = ... //某种Iterable
val iter = coll.iterator
while (iter.hasNext) {
    对 iter.next() // 执行某种操作
}

Seq是一个有先后次序的值的序列,比如数组或列表。IndexedSeq允许我们通过整型的下标快速地访问任意元素。举例来说,ArrayBuffer是带下标地,但链表不是。

Set是一组没有先后次序地值。在SortedSet中,元素是以某种排过序地顺序被访问。

Map是一组(键,值)对偶。SortedMap按照键地排序访问其中地实体。

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

Iterable(0xFF,0xFF00,0xFF0000)
Set(Color.RED,Color.GREEN,Color.BLUE)
Map(Color.RED -> 0xFF0000, Color.GREEN -> 0xFF00, Color.BLUE -> 0xFF)
SortedSet("Hello","World")

13.2 可变和不可变集合

不可变的集合从不改变,因此你可以安全地共享其引用,甚至是一个多线程的应用当中。举例来说,既有scala.collection.mutable.Map,也有scala.collection.immutable.Map。他们有一个共有的超类型scala.collection.Map(当然,这个超类型没有定义任何改值操作)

Scala优先采用不可变集合。scala.collection包中的伴生对象产出不可变的集合。举例来说,scala.collection.Map("Hello"->42)是一个不可变的映射。

不止如此,总被引入的scala包和Predef对象里有指向不可变特质的类型别名List、Set和Map

举例来说,Predef.Map和scala.collection.immuatable.Map是一回事

不可变集合的用途:基于老的集合创建新的集合

如果某个值已经在集中,则你得到的是指向老集的引用。这在递归计算中特别自然。举例来说,你可以计算某个整数中所有出现过的数的集:

def digits(n: Int): Set[Int] = 
	if ( n< 0) digits(-n)
	else if ( n<10) Set(n)
	else digits(n/10) + (n % 10) // 注意:这里的加号不在 IF-ELSE中

13.3 序列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dy1YeqzA-1662286997136)(C:\Users\johnqyang\AppData\Roaming\Typora\typora-user-images\image-20210719101219354.png)]

Vector是ArrayBuffer的不可变版本:一个带下标的序列,支持快速的随机访问。向量是以树形结构的形式实现的,每个节点可以有不超过32个子节点。对于一个有100万个元素的向量而言,我们只需要四层节点(10^6 ~= 32^4)。访问这样一个列表中的某个元素只需要4跳,而在链表中,同样的操作平均需要50W跳。

Range表示一个整数序列,Range对象并不存储所有值而只是起始值,结束值和增值。你可以用to和until方法来构造Range对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ufTFi4q-1662286997136)(C:\Users\johnqyang\AppData\Roaming\Typora\typora-user-images\image-20210719101932425.png)]

我们在第3章中介绍了数组缓冲。而栈、队列、优先级队列等都是标准的数据结构,用来实现特定的算法。

13.4 列表

在Scala中,列表要么是Nil(即空表),要么是一个head元素加上一个tail,而tail又是一个列表。比如:

val digits = List(4,2)
digits.head // 值为4
digits.tail 是 List(2)
digits.tail.head // 是2
digits.tail.tail // 是Nil

::操作符从给定的头和尾创建一个新的列表。例如:

9 :: List(4,2) //  List(9,4,2)
9 :: 4 :: 2 :: Nil // 右结合,从末端开始
9 :: (4 :: (2 :: Nil)

在scala中,链表的递归会比用迭代器自然:

def sum(lst: List[Int]) : Int = 
	if ( lst == Nil) 0 else lst.head + sum(lst.tail)

或者模式匹配:

def sum(lst: List[Int]): Int = lst match {
    case Nil => 0
    case h :: t => h + sum(t) // h是lst.head 而 t是lst.tail
}

或者:

List(9,4,2).sum

13.5 可变列表

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

注意:你并不是给head和tail赋值

例子,把所有负值改为零:

val lst = scala.collection.mutable.LinkedList(1,-2,7,-9)
var cur = lst
while ( cur != Nil) {
    if (cur.elem < 0) cur.elem = 0
    cur = cur.next
}

var cur = lst
while( cur != Nil && cur.next != Nil) {
    cur.next = cur.next.next
    cur = cur.next
}

变量cur用起来就像是迭代器,但实际上它的类型是LinkedList

除了LinkedList外,Scala还提供了一个DoubleLinkedList,区别是它多带一个prev引用。

注意:如果你想要把列表中某个节点变成列表中的最后一个节点,你不能将next引用设为Nil,而应该将它设为LinkedList.empty。也不要将它设为null,不然你会在遍历该链表的时候遇到空指针错误。

13.6 集

集是不重复元素的集合。底层哈希,顺序不一定。其元素根据hashCode方法的值进行组织。

Set(1,2,3,4,5,6) // 可能访问到的次序 5,2,3,1,6

链式哈希集可以记住元素被插入的顺序。它会维护一个链表来达到这个目的,例如:

val weekdays = scala.collection.mutable.LinkedHashSet("MP","TU","WE")

如果你想要按照已排序的顺序来访问集中的元素,用已排序的集:

scala.collection.immutable.SortedSet(1,2,3,4,5,6)

注意:Scala 2.9 没有可变的已排序集。如果你需要这样一个数据结构,可以用java.util.TreeSet

位集(bit set)是集的一种实现,以一个字位序列的方式存放非负整数。如果集中有i,则第i位是1。Scala提供了可变的和不可变的两个BitSet类。

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

val digits = Set(1,7,2,9)
digits contains 0 // false
Set(1,2) subsetOf digits // true

union、intersect和diff方法执行通常的集合操作。如果愿意,可以写成|&&~。也可以将union写成++,将diff写成--。举例:

val primes = Set(2,3,5,7)
digits union primes // Set(1,2,3,5,7,9)
digits & primes // Set(2,7)
digits -- primes // Set(1,9)

13.7 用于添加或去除元素的操作符

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TkrrpcHE-1662286997136)(C:\Users\johnqyang\AppData\Roaming\Typora\typora-user-images\image-20210719111906748.png)]

+用于将元素添加到无先后次序的集合,而+::+则是将元素添加到有先后次序的集合的开头或结尾。

13.8 常用方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fHQmpYTE-1662286997137)(C:\Users\johnqyang\AppData\Roaming\Typora\typora-user-images\image-20210719112309642.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hjhLnIbc-1662286997138)(C:\Users\johnqyang\AppData\Roaming\Typora\typora-user-images\image-20210719112319262.png)]

Seq特质在Iterable特质的基础上又额外添加了一些方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pcjazEPW-1662286997138)(C:\Users\johnqyang\AppData\Roaming\Typora\typora-user-images\image-20210719112408985.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iPDlUoim-1662286997138)(C:\Users\johnqyang\AppData\Roaming\Typora\typora-user-images\image-20210719112415683.png)]

13.9 将函数映射到集合

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方法用于偏函数——那些并没有对所有可能的输入值进行定义的函数。它产出被定义的所有参数的函数值的jihe.liru:

"-3+4".collect { case '+' => 1; case '-' => -1} //Vector(-1,1)

最后,如果你应用函数到各个元素仅仅是为了它的副作用而不关心函数值的话,可以用foreach:

names.foreach(println)

13.10 化简、折叠和扫描

map方法将一元函数应用到集合的所有元素。如果要二元函数,类似c.reduceLeft(op)这样的调用将op相继应用到元素:

List(1,7,2,9).reduceLeft(_ - _)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dlqvEiQy-1662286997139)(C:\Users\johnqyang\AppData\Roaming\Typora\typora-user-images\image-20210719113700704.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tzTEodim-1662286997139)(C:\Users\johnqyang\AppData\Roaming\Typora\typora-user-images\image-20210719113707052.png)]

reduceRight方法做同样的事,只不过它从集合的尾部开始。例如:

List(1,7,2,9).reduceRight(_ - _)

1-(7-(2-9))

以不同于集合首元素的初始元素开始计算通常也很有用。对coll.foldLeft(init)(op)的调用:

List(1,7,2,9).foldLeft(0)(_ - _)
// 0 - 1 - 7 - 2 - 9 = -19

也可以用:/操作符来写foldLeft操作,像这样:

(0 /: List(1,7,2,9))(_ - _)

折叠有时候可以作为循环的替代。例如:

val freq = scala.collection.mutable.Map[Char,Int]()
for ( c <- "Mississippi") freq(c) = freq.getOrElse(c,0) + 1

换种思路,将频率映射和新遇到的字母结合在一起,产生一个新的频率映射。这就是折叠:

(Map[Char,Int]() /: "Mississippi") {
    (m,c) => m + (c -> getOrElse(c,0) +1))
}

每一步计算出一个新的映射

任何while循环都可以用折叠来代替。构建一个把循环中被更新的所有变量结合在一起的数据结构,然后定义一个操作,实现循环中的一步。

最后,在scanLeft和scanRight方法将折叠和映射操作结合在一起。你得到的是包含素有中间结果的集合。例如:

(1 to 10).scanLeft(0)(_ + _)
// 结果:
Vector(0,1,3,6,10,15,21,28,36,45,55)

13.11 拉链操作

前一节的方法是将操作应用到同一集合中的相邻元素。有时,两个集合需要结合在一起。

val prices = List(5.0,20.0,9.95)
val quantities = List(10,2,1)

zip方法让你将他们组合成一个个对偶的列表。例如:

prices zip quantities
// 结果
List[(Double,Int)] = List((5.0,10),(20.0,2),(9.95,1))

应用函数:

(prices zip quantities) map { p => p._1 * p._2}
// 结果
List(50.0,40.0,9.95)
// sum
((prices zip quantities) map { p => p._1 * p._2})sum

如果一个集合比另一个集合短,那么结果中的对偶数量和较短的那个集合的元素数量相同。例如:

List(5.0,20.0,9.95) zip List(10,2)
// 结果
List((5.0,10),(20.0,2))

zipAll方法让你指定较短列表的缺省值:

List(5.0,20.0,9.95).zipAll(List(10,2),0.0,1)
//结果
List((5.0,10),(20.0,2),(9.95,1))

zipWithIndex方法返回对偶的列表,其中每个对偶中第二个组成部分是每个元素的下标。例如:

"Scala".zipWithIndex
// 结果
Vector(('S',0),('c',1),('a',2),('1',3),('a',4))
// 计算具备某种属性的元素的下标
"Scala".zipWithIndex.max
"Scala".zipWithIndex.max._2

13.12 迭代器

iterator方法从集合中得到一个迭代器。

Iterable中有一些方法可以产出迭代器,比如grouped或sliding

有了迭代器,可以用next和hasNext方法来遍历集合中的元素了。

while(iter.hasNext)
	对 iter.next() 执行某种操作
//--------------
for( elem <- iter)
	对elem执行某种操作

上述两种操作迭代器都会移动到集合的末尾,此后就不再被使用了。

Iterator类定义了一些与集合方法使用起来完全相同的方法。。具体而言,除了13.8节列出的所有Iterable的方法,除了head,headOption,last,lastOption,tail,init,takeRight和dropRight外,都支持。再调用诸如map,filter,count,sum甚至是length方法后,迭代器将位于集合的尾端,你不能再继续使用它。而对于其他方法,比如find或take,迭代器位于已找到元素或已取得元素之后。

如果感觉操作迭代器很繁琐,可以使用toArray,toIterable,toSeq,toSet或toMap来将相应的值拷贝到一个新的集合中

13.13 流

13.14 懒视图

流方法是懒执行的,仅当结果被需要时才计算。可以对其他集合应用view方法来得到类似的效果。该方法产出一个其方法总是被懒执行的集合。例如:

val powers = (0 until 1000).view.map(pow(10,_))

将产出一个未被未被求值的集合。(不像流,这里连第一个元素都未被求值。)当你执行:

powers(100)

pow(10,100)被计算,但其他值的幂并没有被计算。和流不同,这些视图并不缓存任何值。如果你再次调用powers(100),pow(10,100)讲被重新计算。

和流一样,用force方法可以对懒视图强制求值。你将得到与原集合相同类型的新集合。

懒集合对于处理那种需要以多种方式进行变换的大型集合是很有好处的,因为它避免了构建出大型中间集合的需要。比较:

(0 to 1000).map(pow,_).map(1 / _)
(0 to 1000).view.map(pow(10,_)).map(1 / _).force

前一个将会计算出10的n次方的集合,然后再对每一个得到的值取倒数。而后一个产出的是记住了两个map操作的视图。当求值动作被强制执行时,对于每个元素,这两个操作被同时执行,不需要额外构建中间集合。

13.15 与java集合的互操作

13.16 线程安全的集合

多线程访问一个可变集合,需确保不会在其他线程对正在访问的进行修改。Scala类库提供了六个特质,你可以将他们混入集合,让集合的操作变成同步的:

SynchronizedBuffer
SynchronizedMap
SynchronizedPriorityQueue
SynchronizedQueue
SynchronizedSet
SynchronizedStack

举例:

val scores = new scala.collection.mutable.HashMap[String,Int] with scala.collection.mutable.SynchronizedMap[String,Int]

通常来说,你最好使用java.util.concurrent包中的某个类。举例来说,如果多线程共享一个映射,那么就用ConcurrentHashMap或ConcurrentSkipListMap。这些集合比简单地用同步方式执行所有方法地映射更高效。不同线程可以并发地访问数据结构中互不相关地部分。(请勿尝试自己实现!)除此之外,对应地迭代器是“弱一致性”的,意思是说,它提供的视图是迭代器被获取时数据结构的样子。

正如前一个节描述的,可以将java.util.concurrent中的集合转换成Scala集合来使用。

13.17 并行集合

如果coll是个大型集合,则:

coll.par.sum

上述代码会并发地对它求和。par方法会产出当前集合的一个并行实现。该实现会尽可能的并行执行集合方法。例如:

coll.par.count(_ % 2 == 0)

对数据,缓冲,哈希表,平衡树而言,并行实现会直接重用底层实际集合的实现,而这是很高效的。

可以通过对要遍历的集合应用.par并行化for循环,就像这样:

for(i <- (0 until 100).par) print(i + " ")

而在for/yield循环中,结果是依次组装的。

for ( i <- (0 until 100).par) yield i + " "

par方法返回的并行集合的类型为扩展自ParSeqParSet,或ParMap特质的类型,所有的这些特质都是ParIterable的子类型。这些并不是Iterable的子类型,因此你不能将并行集合传递给预期IterableSeqSetMap的方法。你可以用ser方法将并行集合转换回串行版本,也可以实现接受通用的GenIterableGenSeqGenSetGenMap类型的参数的方法。

说明并不是所有的方法都可以被并行化。

第14章 模式匹配和样例类

总结TIPS:

  • case _ if Character.isDigit(ch) 。模式匹配的守卫

  • 模式中的变量:匹配的表达式会被赋值给那个变量

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

  • 匹配数组的内容。本质是提取器机制——带有从对象中提取值的unapply或unapplySeq方法的对象

  • 变量声明中也可以用模式匹配。左边是变量,右边是对应的匹配。

  • for推导式中也可以使用模式匹配。典型的就是(k,v)。

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

  • 样例类与样例对象:

    abstract class Amount
    case class Dollar(value: Double) extends Amount
    case class Currency(value: Double, unit: String) extends Amount
    case object Nothing extends Amount
    
    amt match {
        case Dollar(v) => "$" + v
        case Currency(_,u) => "I got" + u
        case Nothing => ""
    }
    //样例类使用(),样例对象不使用圆括号
    
  • 匹配嵌套结构,使用-*表示多个。并使用@表示法将嵌套的值绑定到变量。art @ Article(_,_)

14.1 更好的switch

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

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

Scala的模式匹配没有break也不会进行下一个case。

由于Scala是函数式编程,表达式也是值,则:

sign = ch match {
    case '+' => 1
    case '-' => -1
    case _ => 0
}

可以在match表达式中使用任何类型,而不仅仅是数字。例如:

color match {
    case Color.RED => ...
    case Color.BLACK => ...
    ...
}

14.2 守卫

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

守卫可以是任何Boolean条件。就是让某些满足条件的独立成一个。不像C++中 要是全部都是数字得全部列出。

注意模式总是从上往下进行匹配。

14.3 模式中的变量

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

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

你可以将case _ 看作是这个特性的一种特殊情况,只不过变量名是_罢了。

你可以在守卫中使用这个变量:

str(i) match {
    case ch if Character.isDigit(ch) => digit = Character.digit(ch,10)
    ...
}

注意:变量模式可能会与常量表达式相冲突,例如:

import scala.math._
x match {
    case Pi => ....
    ...
}

Scala是如何知道Pi是常量,而不是变量的呢?背后的规则是,变量必须以小写字母开头。

如果你有一个小写字母开头的常量,则需要将它包在反引号里:

import java.io.File._
str match {
    case `pathSeparator` => ...
    ...
}

14.4 类型模式

对表达式的类型进行匹配,例如:

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

更倾向于使用这样的模式匹配,而不是isInstanceOf操作符。

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

obj match {
    case _: BigInt => Int.MaxValue //匹配任何类型为BigInt的对象
    case BigInt => -1 //匹配类型为Class的BigInt对象
}

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

case m: Map[String,Int] => ... // 别这样做
case m: Map[_, _] => ... // OK

但是对于数组而言其类型信息是完好的。可以匹配Array[Int]

14.5 匹配数组、列表和元组

匹配数组,使用Array表达式:

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

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

匹配列表,同样的方式。可以使用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,0) => y + " 0"
    case _ => "neither is 0"
}

请注意变量是如何绑到列表或元组的不同部分的。由于这种绑定让你可以很轻松地访问复杂结构地各个组成部分,因此这样地操作被称为”析构“。

14.6 提取器

匹配的原理:提取器机制——带有从对象中提取值的unapply或unapplySeq方法的对象。这些方法的实现在第11章。unapply方法用于提取固定数量的对象;而unapplySeq提取的是一个序列,可长可短。

例如:

arr match {
    case Array(0,x) => ...
    ...
}

Array伴生对象就是一个提取器——它定义了一个unapplySeq方法。该方法被调用时,是以被执行匹配动作的表达式作为参数,而不是模式中看上去像是参数的表达式。Array.unapplySeq(arr)产出一个序列值,即数组中的值。第一个值与零进行比较,而第二个被赋值给x。

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

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

注意,这里的提取器并非是一个伴生对象,而是一个正则表达式对象。

14.7 变量声明中的模式

变量声明中也可以使用这样的模式。例如:

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

同时把x=1,y=2。这对于返回对偶的函数很有用:

val (q,r) = BigInt(10) /% 3

也可以用于任何带有变量的模式。例如:

val Array(first,secont, _*) = arr

上述代码将数组arr的第一个和第二个元素分别赋值给first和secong。

14.8 for表达式的模式

for推导式中使用带变量的模式。对每一个遍历到的值,这些变量都会被绑定。这使得我们可以方便地遍历映射:

import scala.collection.JavaConversions.propertiesAsScalaMap
// java Properties -> Scala映射
for ((k,v) <- System.getProperties()) {
    println(k + " -> " v)
}

在for推导式中,如果匹配失败,会被忽略。

for ((k,v) <- System.getProperties() if v == "") {
    println(k)
}

14.9 样例类

特殊的类,经过优化以被用于模式匹配。例子,两个扩展自常规类的样例类:

abstract class Amount
case class Dollar(value: Double) extends Amount
case class Currency(value: Double, unit: String) extends Amount

你也可以针对单例的样例对象:

case object Nothing extends Amount

当我们有一个类型为Amount的对象时,我们可以用模式匹配来匹配到它的类型,并将属性值绑定到变量:

amt match {
    case Dollar(v) => "$" + v
    case Currency(_, u) => "Oh noes, I got " + u
    case Nothing => ""
}

说明:样例类的实例使用(),样例对象不使用圆括号。

当你声明样例类时,有如下几件事自动发生:

  • 构造器中的每一个参数都成为val —— 除非它被显式地声明为var(不建议这样做)
  • 在伴生对象中提供apply方法让你不用new关键字就能构造出相应的对象,比如Dollar(29.9)
  • 提供unapply方法让模式匹配可以工作——参照第11章
  • 将生成toString,equals、hashCode和copy方法——除非显式地给出这些方法地定义。

除了上述外,样例类和其他类完全一样。你可以添加字段和方法,扩展他们等等

14.10 copy方法和带名参数

样例类的copy方法创建一个与现有对象值相同地新对象。例如:

val amt = Currency(29.9,"EUR")
val price = amt.copy()

可以使用带名参数修改某些属性:

val price = amt.copy(value = 19.95)
val price = amt.copy(unit = "CHF")

14.11 case语句中的中置表示法

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

amt match { case a Currency u => ...} //等同于 case  Currency(a,u)

这个特性本意是要匹配序列。举例来说,每个List对象要么是Nil,要么是样例类::,定义如下:

case class ::[E](head: E, tail: List[E]) extends List[E]

因此,可以这样写:

lst match { case h :: t => ...}
//等价于case ::(h,t),将调用::.unapply(lst)

解析结果组合在一起的~样例类。本意同样是中置表达式的形式用于case语句:

result match { case p~q => ...} //等同于 case ~(p,q)

当你把多个中置表达式放在一起的时候,他们会更易读。例如:

result match { case p ~ q ~ r => ...}

等价于~(~(p,q),r)

如果操作符是以冒号结尾,则它是从右向左结合的。例如:

case first :: second :: rest
case ::(first,::(second,rest))

14.12 匹配嵌套结构

样例类经常被用于嵌套结构。例如,有时我们会将物品捆绑在一起打折出售:

abstract class Item
case class Article(description: String, price: Double) extends Item
case class Bundle(description: String,discount: Double,items: Item*) extends Item

因为不用使用new,所以我们可以很容易地给出嵌套对象定义:

Bundle("Father",20.0,
      Article("Scala for Impatient",39.95),
      Budile("Anchor Distillery Sampler",10.0,
            Article("Old Potrero Rye Whisky",79.95),
            Article("Junipero Gin",32.95)))

模式可以匹配到特定地嵌套,比如:

case Bundle(_,_,Article(descr,_),_*) => ...

上述代码将descr绑定到Bundle地第一个Article地描述。

你也可以用@表示法将嵌套地值绑定到变量:

case Bundle(_,_, art @ Article(_,_), rest @ _*) => ....

这样,art就是Bundle中地第一个Article,而rest则是剩余地Item地序列。

注意,本例中_*是必须地。以下模式:

case Bundle(_,_, art @ Article(_,_),rest) => ...

将只能匹配到那种只有一个Article再加上不多不少正好一个Item地Bundle,而这个Item将被绑定到rest变量。

以下似乎计算某Item价格地函数:

def price(it: Item): Double = it match {
    case Article(_,p) => p
    case Bundle(_,disc, its @ _*) => its.map(price _).sum * disc
}

14.13 样例类是evil的吗

样例类适用于那种标记不会改变的结构。举例来说,Scala的List就是用样例类来实现的。简化一些细节,列表从本质上说就是:

abstract class List
case object Nil extends List
case class ::(head: Any, tail: List) extends List

列表要么是空,要么是一头一尾。没人会增加出一个新的样例。(下一节会看到如何组织别人这样做)

优势:

  • 模式匹配通常比继承更容易把我们引向更精简的代码。
  • 构造时不需要用new的复合对象更加易读
  • 你将免费得到toString,equals,hashCode和copy方法

对于某种特定种类的类,样例类会提供给你的是完全正确的语义。有人将他们称为值类。例如Currency类:

case class Currency(value: Double, unit: String)

一个Currency(10,“EUR”)和Current(10,“EUR”)是等效的。这样的类通常是不可变的。

可变字段的样例类,我们应该总是从那些不会被改变的字段来计算和得出其哈希码。

14.14 密封类

让编译器帮你确保你已经列出了所有可能的选择。要达到这个目的,你需要将样例类的通用超类声明为sealed

sealed abstract class Amount
case class Dollar(value: Double) extends Amount
case class Currency(value: Double, unit: String) extends Amount 

密封类的所有子类都必须再与该密封类相同的文件中定义。举例来说,如果有人想要为欧元添加另一个样例类:

case class Euro(value: Double) extends Amount

他们必须在Amount被声明的那个文件中完成。

如果某个类是密封的,那么在编译期所有子类就是可知的,因而编译器可以检查模式语句的完整性。让所有(同一组)样例类都扩展某个密封的类或特质是个好的做法。

14.15 模拟枚举

样例类让你可以在Scala中模拟出枚举类型。

sealed abstract class TrafficLightColor
case object Red extends TrafficLightColor
case object Yellow extends TrafficLightColor
case object Green extends TrafficLightColor
color match {
    case Red => "stop"
    case Yello => "hurry up"
    case Green => "Go"
}

如果觉得这样实现有点笨重,可以使用我们在第6章介绍过的Enumeration助手工具。

但这样可以让编译确认所有枚举值被列出。

14.16 Option类型

标准类库中的Option类型用样例类来表示那种可能存在、也可能不存在的值。样例子类Some包装了某个值,例如:Some(“Fred”)。而样例对象None表示没有值。

这比使用空字符串的意图更加清晰,比使用null来表示缺少某值的做法更加安全。

Option支持泛型。举例来说Some("Fred")的类型是Option[String]

Map类的get方法返回一个Option。如果对于给定的键没有对应的值,则get返回None。如果有值,就会将该值包在Some中返回。

你可以用模式匹配来分析这样的一个值:

scores.get("Alice") match {
    case  Some(score) => println(score)
    case None => println("No Score")
}

不过这比较繁琐。可以使用isEmptyget

val alicesScore = scores.get("ALICE")
if (alicesSocre.isEmpty) println("No score")
else println(alicesScore.get)

这也很繁琐。可以使用:

println(alicesScore.getOrElse("No socre"))

其实,Map类也提供了getOrElse方法:

println(socres.getOrElse("Alice","No Score"))

如果你想要忽略None值,可以用for推导式:

for(score <- scores.get("Alice")) println(score)

如果get方法返回None,什么也不发生。如果返回Some,则score将被绑定到它的内容。

你也可以将Option当作是一个要么是空,要么带有单个元素的集合,并使用诸如map,foreach或filter等方法。例如:

scores.get("Alice").foreach(println _)

上述代码将打印分数,或者如果get返回None的话,什么也不做。

14.17 偏函数

第15章 注解

15.1 什么是注解

注解语法和Java一样,例如:

@Test(timeout = 100) def testSomeFeature() {...}

@Entity class Credentials {
    @Id @BeanProperty var username: String _
    @BeanProperty var password: String _
}

你可以对Scala使用Java注解,上述的示例中注解(除了@BeanProperty)来自JUnitJPA,而这两个Java框架并不知道我们用的是Scala

也keyi使用Scala注解。这些注解是Scala特有的,通常由Scala编译器或编译器插件处理。

Java注解不影响编译器的编译。而Scala中,注解可以影响编译过程。举例来说,第5章中@BeanProperty注解将触发get和set方法的生成。

15.2 什么可以被注解

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

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

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

class Credentials @Inject()(var username: String, var password: String)

你还可以为表达式添加注解。你需要在表达式后加上冒号,然后是注解本事:

(myMap.get(key): @unchecked) match {...}

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

class MyContainer[@specialized T]

针对实际类型的注解应放置在类型名称之后,就像这样:

String @cps[Unit] // @cps带一个类型参数 第22章

15.3 注解参数

Java注解可以有带名参数,比如:

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

不过,如果参数名为value,则该名称可以直接忽略。例如:

@Named("creds") var credentials: Crendentials = _
// value参数的值为"creds"

如果注解不带参数,则圆括号可以省去:

@Entity class Crendentials

大多数注解参数都有缺省值。举例来说,JUnit的@Test注解的timeout参数有一个缺省的值为0,表示没有超时。而expected参数有一个假的缺省类来表示不预期的任何异常。

如果使用:

@Test def testSomeFeature() {...}

这个注解等同于:

@Test(timeout = 0, expected = classOf[org.junit.Test.None])
def testSomeFeature() {...}

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

  • 数值型的字面量
  • 字符串
  • 类字面量
  • Java枚举
  • 其他注解
  • 上述类型的数组(但不能是数组的数组)

Scala注解的参数可以是任何类型,但只有少数几个Scala注解利用了这个增加处理的灵活性。举例来说,@deprecatedName注解有一个类型为Symbol的参数。

15.4 注解实现

明白注解类是如何实现的。

注解必须扩展自Annotation特质。例如,unchecked注解定义如下:

class unchecked extends annotation.Annotation

注解类可以选择扩展自StaticAnnotationClassfileAnnotation特质。StaticAnnotation在编译单元中可见——它将放置Scala特有的元数据到类文件中,而ClassfileAnnotaion的本意是在类文件中生成Java注解元数据。不过,Scala 2.9并未支持该功能。

Scala字段定义可能会引出多个Java特性,而他们都有可能被添加注解。举例来说,有如下定义:

class Credentials(@NotNull @BeanProperty var username: String)

在这里,总共有六个可以被注解的目标:

  • 构造器参数
  • 私有的示例字段
  • 取值器方法username
  • 改值器方法username_=
  • bean取值器getUsername
  • bean改值器setUsername

默认情况下,构造器参数注解仅会被应用到参数自身,而字段注解只能应用到字段。元注解@param,@field,@getter,@setter,@beanGetter和@beanSetter将使得注解被附在别处。举例来说,deprecated注解的定义如下:

@getter @setter @beanGetter @beanSetter
class deprecated(message:String = "",since: String = "")
	extends annotation.StaticAnnotation

你也可以根据临时需要应用这些元注解:

@Entity class Credentials  {
    @(Id @beanGetter) @BeanProperty var id = 0
    ...
}

在这种情况下,@Id注解将被应用到Java的getId方法,这是JPA要求的方法,用来访问属性字段。

15.5 针对Java特性的注解

15.6 用于优化的注解

15.7 用于错误和警告的注解

如果你给某个特性加上了**@deprecated**注解,则每当编译器遇到对这个特性的使用时都会生成一个警告信息。该注解有两个选填参数,message和since。

@deprecated(message = "Use factorial(n: BigInt) instead")
def factorial(n: Int): Int = ...

@deprecatedName可以被应用到参数上,并给出一个该参数之前使用过的名称。

def draw(@deprecatedName('sz) size: Int,style: Int = NORMAL)

仍然可以调用draw(sz=12),不过你将会得到一个表示该名称已过时的警告。

以单引号开头的名称。

@implicitNotFound注解用于在某个隐式参数不存在的时候生成有意义的错误提示。见21章。

@unchecked注解用于在匹配不完整时取消警告信息。举例来说,假定我们知道某个列表不可能为空:

(lst: @unchecked) match {
    case head :: tail => ...
}

编译器不会报告说没给Nil选项。如果lst是Nil,则运行期会报出异常。

@uncheckedVariance注解会取消与型变相关的错误提示。举例来说,java.util.Comparator按理应该是逆变的。如果Student是Person的子类型,那么在需要Comparator[Student]的时候,我们也可以使用Comparator[Person]。但是,Java的泛型不支持型变。我们可以通过@uncheckedVariance注解来解决这个问题:

trait Comparator[-T] extends
	java.lang.Comparator[T @uncheckedVariance]

17 类型参数

17.1 泛型类

class Pair[T,S](val first: T,val seconds: S)

scala会从构造参数推断出实际类型

val p = new Pair(42,"String")

也可以自己指定类型:

val p2  = new Pair[Any,Any](42,"String")

17.2 泛型函数

def getMiddle[T](a: Array[T]) = a(a.length / 2)

你需要把类型参数放在方法名之后。

scala会从调用该方法使用的实际参数来推断类型。

getMiddle(Array("Mary","a"))

如果有必要,你可以指定类型:

val f = getMiddle[String] _ // 这是具体的函数,保存到f

17.3 类型变量界定

对类型进行界定。添加一个上界T<:Comparable[T]。由于这个类内有些方法不是所有的类适用。

class Pair[T <: Comparable[T]](val first: T, val second: T) {
    def samller = if(first.compareTo(second) < 0) first else second
}

这意味着T必须是Comparable[T]的子类型。

添加一个下界 >:。传入的类型必须是原类型的超类型。

def replaceFirst[R >: T](newfirst: R) = new Pair[R](newfirst,second)

17.4 视图界定

允许隐式转换到类型。

class Pair[T <% Comparable[T]]

17.5 上下文界定

视图界定要求必须存在一个从T到V的隐式转换。

上下文界定的形式为T:M,其中M是另一个泛型类。它要求必须存在一个类型为M[T]的隐式值。

class Pair[T: Ordering](val first: T,val second: T) {
    def smaller(implicit ord: Ordering[T]) = 
    	if (ord.compare(first,second) < 0) first else second
}

必须存在一个类型为Ordering[T]的隐式值。该隐式值可以被用在该类的方法中。

17.6 Manifest上下文界定

实例化一个泛型的Array[T],需要一个Manifest[T]对象。

如果你要编写一个泛型函数来构造泛型函数的话,你需要传入这个Manifest对象来帮忙。由于它是构造器的隐式参数,你可以用上下文界定:

def makePair[T : Manifest](first: T, seconde: T) {
    val r = new Array[T](2); r(0) = first; r(1) = second; r
}

如果你调用makePari(4,9),编译器会定位到隐式的Manifest[Int]并实际上调用makePair(4,9)(intManifest)。这样一来,该方法调用的就是new Array(2)(intManifest),返回基本类型的数组int[2]。

在虚拟机中,泛型相关的类型信息是被抹掉的。

17.7多重界定

T >: Lower <: Upper
T <: Comparable[T] with Serializable with Cloneable
T <% Comparable[T] <% String //多个视图界定
T : Ordering : Manifest //多个上下文界定

17.9 型变

class Pair[+T] // 代表与T协变,也就是说,它与T按同样的方向型变。由于Student是PERSON的子类,Pair[Student]也就是Pair[Person]的子类型了。

协变

trait Friend[-T] {
    def beFriend(someone: T)
}

逆变。采用超类型。

21 隐式转换与隐式参数

隐式对象是如何被自动呼出用于执行转换或其他任务的。

21.1 隐式转换

隐式转换函数:以implicit关键字声明的带有单个参数的函数。

这样的函数将被自动应用,将值从一种类型转换为另一个类型。

implicit def int2Fraction(n: Int) = Fraction(n,1)
//使用
val result = 3 * Fraction(4,5)//将调用int2Fraction(3)

隐式转换函数将整数3转换成了一个Fraction对象。这个对象接着又被乘以Fraction(4,5)

可以给隐式转换函数起任何名称。由于你并不显式地调用它。但建议使用source2Target这种命名方式。

21.2 利用隐式转换丰富现有类库的功能

用处:当希望某个类有某个方法,而这个类的作者没有提供的时候。

class RichFile(val from: File) {
    def read = Source.fromFile(from.getPath).mkString
}
implicit def file2RichFile(from: File) = new RichFile(from)

将原来的类型转换到新的类型,再在新的类型定义想要有的方法。

21.3 引入隐式转换

Scala会考虑如下的隐式转换函数:

  • 位于源或目标类型的伴生对象中的隐式函数
  • 位于当前作用域可以以单个标识符指代的隐式函数

比如int2Fraction函数。我们可以将它放到Fraction伴生对象中,这样它就能够被用来将整数转换为分数了。

或者,假定我们把它放到了FractionConversions对象当中,而这个对象位于com.horstmann.impatient包。如果你想要使用这个转换,就需要引入FractionConversions对象,例如:

import com.horstmann.impatient.FractionConversions._ //一定要有._

int2Fraction方法只能以FractionConversions.int2Fraction的形式被任何想要显式调用它的人使用。但如果该函数不能直接以int2Fraction访问到,不加限定词的话,编译器是不会使用它的。

可以引入局部化以尽量避免不想要的转换发生:

object Main extends App {
    import com.horstmann.impatient.FractionConversions._
    xxx
}

如果某个特定的隐式转换给你带来麻烦,你可以将它排除在外:

import com.horstmann.impatient.FractionConversions.{fractions2Double => _,_}

21.4 隐式转换规则

隐式转换在如下三种各不相同的情况会被考虑:

  • 当表达式的类型与预期的类型不同时:sqrt(Fraction(1,4))
  • 当对象访问一个不存在的成员时:new File("a").read
  • 当对象调用某个方法,而该方法的参数声明与传入参数不匹配时:3 * Fraction

另一方面,有三种情况编译器不会尝试使用隐式转换:

  • 如果代码可以在不使用隐式转换的前提下通过编译,那么就不会使用隐式转换。
  • 编译器不会尝试同时执行多个转换
  • 存在二义性的转换是个错误。举例:convert1(a) * bconvert2(a) * b
scalac -Xprint:typer MyProg.scala // 显式隐式转换

21.5 隐式参数

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

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

在这种情况下,编译器将会查找一个类型为Delimiters的隐式值。

这必须是一个声明为implicit的值。编译器将会在如下两个地方查找这样的一个对象:

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

比如:

object FrenchPunctuation {
    implicit val quoteDelimiters = Delimiters("<<",">>")
}
import FrenchPunctuation._

如此一来,就可以引入隐式值。

对于给定的数据类型,只能有一个隐式的值。因此,使用常用类型的隐式参数不是一个好主意。例如:

def quote(what: String)(implicit left: String, right: String) // 别这样做

上述代码行不通,因为调用者没法提供两个不同的字符串。

21.6 利用隐式参数进行隐式转换

隐式的函数参数可以被用作隐式转换。

def smaller[T](a: T,b: T)(implicit order: T => Ordered[T]) 
  = if (order(a) < b) a else b

由于Ordered[T]特质有一个接受T作为参数的<操作符。因此这个版本是正确的。

order是带有单个参数的函数,被打上了implicit标签,并且有一个以的那个标识符出现的名称。因此,它不仅是一个隐式参数,它还是一个隐式 转换。正因为如此,函数体可以略去对order的显式调用:

def smaller[T](a: T,b: T)(implicit order: T=> Ordered[T])
  = if (a<b) a else b

如果你想要调用:

smaller(Fraction(1,2),Fraction(2,1))

就需要定义一个Fraction=> Ordered[Fration]的函数,要么在调用的时候显式写出,或者定义为一个implicit val。

第18章 高级类型

18.1 单例类型

给定任何引用v,你可以得到类型v.type,它有两个可能的值:vnull

我们来看那种返回this的方法,通过这种方法你可以把方法调用串接起来:

class Documnet {
    def setTitle(title: String) = {..;this}
    def setAuthor(author: String) = {...; this}
}

这样的话,你就可以编写如下的代码:

article.setTitle("..").setAuthor("cay")

但是,如果你还有子类的话,那么问题来了:

class Book extends Document {
    def addChapter(chapter: String) = {...; this}
}
val book = new Book()
book.setTitle("xx").addChapter(chapter1) // 错误

由于setTitle返回的是this,scala将返回类型推断为Document。但Documnet并没有addChapter方法

解决方法是声明setTitle的返回类型为this.type:

def setTitle(title: String): this.type = {...; this}

如果你想要定义一个接受object实例作为参数的方法,你也可以使用单例类型。可能会问:你什么时候才会这样做呢,毕竟如果只有一个实例,方法直接用它就好了,为什么还要传入呢?有些人喜欢构造那种读起来像英文的流利接口。

book set Title to "Scala"
object Title
class Document {
    private var useNextArgAs: Any = null
    def set(obj: Title.type): this.type = {useNextArgAs = obj; this }
    def to(arg: String) = if (useNextArgAs == Title) title = arg; else ...
}

注意Title.type参数。

18.2 类型投影

嵌套类从属于包含它的外部对象。

import scala.collection.mutable.ArrayBuffer
class Network {
    class Member(val name: String) {
        val contacts = new ArrayBuffer[Member]
    }
    
    private val members = new ArrayBuffer[Member]
    
    def join(name: String) = {
        val m = new Member(name)
        members += m
        m
    }
}

每个网络实例都有它自己的member类。举例:

val chatter = new Network
val myFace = new Network
//现在chatter.Member和myFace.Member是不同的类

你不能将其中一个网络的成员添加到另一个网络。

如果你不希望有这样的约束,你应该把Member类直接挪到Network类之外。一个好的地方可能是Network的伴生对象中。

如果你要的就是细粒度的类,只是偶尔想使用更为松散的定义,那么可以用类型投影Network#Member,意思是任何Network的Member。

class Network {
    class Member(val name: String) {
        val contracts = new ArrayBuffer[Network#Member]
    }
}

18.4 类型别名

对于复杂的类型,你可以用type关键字创建一个简单的别名,就像这样:

class Book {
    import scala.collection.mutable._
    type Index = HashMap[String,(Int,Int)]
}

类型别名必须被嵌套在类或对象中。它不能出现在scala文件的顶层。

18.5 结构类型

指的是一组关于抽象方法、字段和类型的规格说明,这些抽象方法、字段和类型是满足该规则的类型必须具备的。

举例来说:如下方法带有一个结构类型参数target:

def appendLines(target: {def append(str: String): Any},lines: Iterable[String]) {
    for()...
}

你可以对任何具备append方法的类的实例调用appendLines方法。这比定义一个Appendable特质更为灵活,因为你可能并不总是能够将该特质添加到使用的类上。

在幕后,scala使用反射来调用target.append(..)。结构类型让你可以安全而方便地做这样的反射调用。

不过,相比常规方法调用,反射方法调用的开销大得多。因此,你应该只在需要抓住那些无法共享一个特质的类得到共通行为的时候才使用结构类型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值