目录:
概述 1
概述
scala是一门以java虚拟机(JVM)为目标运行环境并将面向对象和函数式编程的最佳特性结合在一起的静态类型编程语言。
scala是纯粹的面向对象的语言。java虽然是面向对象的语言,但是它不是纯粹的,因为java的基本数据类型不是类,并且在java中还有静态成员变量和静态方法。相反,scala是纯粹面向对象的,每个值都是对象,每个操作都是方法调用。
scala也是一个成熟的函数式语言。函数式编程有两个指导思想:①函数是头等值,也就是说函数也是值,并且和其他类型(如整数、字符串等)处于同一地位,函数可以被当作参数传递,也可以被当作返回值返回,还可以在函数中定义函数等等;②程序的操作应该把输入值映射为输出值而不是就地修改,也就是说函数调用不应产生副作用,虽然函数式编程语言鼓励使用“无副作用”的方法,但是scala并不强制你必须这么做。scala允许你使用指令式的编程风格,但是随着你对scala的深入了解,你可能会更倾向于一种更为函数式的编程风格。向函数式编程转变,你就应该尽量去使用val、不可变对象、无副作用方法,而不是var、可变对象、有副作用方法。要明白的是,从指令式编程向函数式编程的转变会很困难,因此你要做好充分的准备,并不断的努力。
scala运行于JVM之上,并且它可以访问任何的java类库并且与java框架进行互操作,scala也大量重用了java类型和类库。
第一个scala程序:
object ScalaTest {
def main(args: Array[String]) {
println(“hello scala.”)
}
}
Boolean true或者 false)
Byte 8位, 有符号
Short 16位, 有符号
Int 32位, 有符号
Long 64位, 有符号
Char 16位, 无符号
Float 32位, 单精度浮点数
Double 64位, 双精度浮点数
String 其实就是由Char数组组成
说明:
1.Scala并不区分基本类型和引用类型,所以这些类型都是对象,可以调用相对应的方法。
2.String直接使用的是java.lang.String. 不过,由于String实际是一系列Char的不可变的集合,Scala中大部分针对集合的操作,都可以用于String,具体来说,String的这些方法存在于类scala.collection.immutable.StringOps中。
3.每一种数据类型都有对应的Rich* 类型,如RichInt、RichChar等,为基本类型提供了更多的有用操作。
4.这些类型都是抽象的final类,不能使用new新建,也不能被继承。
5.在scala中,常量也称作字面量,字符串字面量由双引号包含的字符组成,同时scala提供了另一种定义字符串常量的语法——原始字符串,它以三个双引号作为开始和结束,字符串内部可以包含无论何种任意字符。
6.使用方法,而不是强制类型转换,来做数值类型之间的转换,如99.44.toInt、97.toChar。另外也可以参见显式类型转换和隐式转换。
1.2 数据类型结构图
Scala中,所有的值都是类对象,而所有的类,包括值类型,都最终继承自一个统一的根类型Any。
统一类型,是Scala的又一大特点。更特别的是,Scala中还定义了几个底层类(Bottom Class),比如Null和Nothing。
说明:
Any是所有其他类的超类。Null是所有引用类(继承自AnyRef的类)的子类,Null类型的值为null。Nothing是所有其他类(包括Null)的子类,Nothing类型没有任何值,它的一个用处是它标明了不正常的终止(例如抛出异常,啥也不返回)。AnyVal是scala中内建值类(共9个)的父类。AnyRef是scala中所有引用类的父类,在java平台上AnyRef实际就是java.lang.Object的别名,因此java里写的类和scala里写的类都继承自AnyRef,你可以认为java.lang.Object是scala在java平台上实现AnyRef的方式。scala类与java类的不同之处在于,scala类还继承了一个名为ScalaObject的特别记号特质,目的是想让scala程序执行得更高效。
Any类的成员方法:
方法定义 属性 说明
(1) def getClass(): Class[_] : 抽象 返回运行时对象所属的类的表示
(2) final def !=(arg0: Any): Boolean: 具体 比较两个对象的自然相等性是否不相等
(3) final def (arg0: Any): Boolean: 具体 比较两个对象的自然相等性是否相等
(4) def equals(arg0: Any): Boolean: 具体 比较两个对象的自然相等性,被!=和 调用
(5) final def ##(): Int: 具体 计算对象的哈希值,等同于hashCode,但是自然相等性相等的两个对象会得到相同的哈希值,并且不能计算null对象
(6) def hashCode(): Int: 具体 计算对象的哈希值
(7) final def asInstanceOf[T]: T: 具体 把对象强制转换为T类型
(8) final def isInstanceOf[T]: Boolean: 具体 判断对象是否属于T类型,或T的子类
(9) def toString(): String: 具体 返回一个字符串来表示对象
1.3 变量
scala有两种变量:val和var。由于scala是完全面向对象的,因此val和var只是声明了对象的引用是不可变的还是可变的,并不能说明引用指向的对象的可变性。声明变量的同时需要初始化之,否则该变量就是抽象的。如果不指定变量的类型,编译器会从初始化它的表达式中推断出其类型。当然你也可以在必要的时候指定其类型,但注意,在scala中变量或函数的类型总是写在变量或函数的名称的后边。
示例如下:
val answer = “yes”
val answer, message: String = “yes”
1.4 懒值
当val被声明为lazy时,他的初始化将被推迟,直到我们首次对此取值,适用于初始化开销较大的场景。
lazy示例:通过lazy关键字的使用与否,来观察执行过程
object Lazy {
def init(): String = {
println(“init方法执行”)
“嘿嘿嘿,我来了~”
}
def main(args: Array[String]): Unit = {
lazy val msg = init()
println(“lazy方法没有执行”)
println(msg)
}
}
1.5 操作符
操作符即方法。反过来,方法也可以写成操作符的形式。
举例: scala> 1 + 2
等同于:scala> 1.+(2)
1)前缀操作符
只接收一个操作元(数),操作符在前、操作元在后。前缀操作符只有+ - ! ~四个,与之对应的方法名称是“unary_”加上操作符。 如果定义了某个方法的名称为“unary_”加上这四个操作符之外的操作符,那么只能当方法调用而不能写成前缀操作符的形式。如:
-2.0相当于(2.0).unary_-
2)中缀操作符
接收两个操作元,一左一右,其中一个是调用该方法的对象,一个是传入该方法的参数。
注意,以冒号“:” 结尾的操作符,右操作元是调用该方法的对象,其余操作符默认左操作元是调用该方法的对象.
3)后缀操作符
只接收一个操作元,该操作元是调用方法的对象。对应的方法不接收参数,写成后缀操作符形式时, 省略“.”和空括号。
s toLowerCase 相当于 s.toLowerCase
尖叫提示:Scala中没有++、–操作符,需要通过+=、-=来实现同样的效果。
1.6 option类型
Scala为单个值提供了对象的包装器,表示为那种可能存在也可能不存在的值。他只有两个有效的子类对象,一个是Some,表示某个值,另外一个是None,表示为空,通过Option的使用,避免了使用null、空字符串等方式来表示缺少某个值的做法。
引入 Option 的原因是: 有时候确实需要某些变量是可选值,要么有值,要么无值。 在没有 Option 的时候, 无值是用 null 来表示, 这就必须对变量进行判空操作。 在 Java 里判空是一个运行时的动作, 如果忘记判空, 编译时并不会报错,但是在运行时可能会抛出空指针异常。 而 Option 的引入, 首先从字面上提醒读者这是一个可选值;其次,最重要的是由于Option 类型与普通类型不一样, 要使用 Option 类型必须先做相应的类型转换。类型转换最常见的方式就是模式匹配,在这期间可以把无值 None 过滤掉。如果不进行类型转换,编译器就会抛出类型错误,这样在编译期就进行判空处理进而防止运行时出现更严重的问题。
val map1 = Map(“Alice” -> 20, “Bob” -> 30)
println(map1.get(“Alice”))
println(map1.get(“Jone”))
1.7 块表达式与赋值
在scala中,{}块包含一系列表达式,其结果也是一个表达式,块中最后一个表达式的值就是其值。在scala中,赋值语句本身的值是Unit类型的。因此如下语句的值为“()”:
{r = r * n; n -= 1}
正是由于上述原因,scala中不能多重赋值,而java和c++却可以多重赋值。因此,在scala中,如下语句中的x值为“()”:
x = y = 1
2. 控制结构和函数
scala和其他编程语言有一个根本性差异:在scala中,几乎所有构造出来的语法结构都有值。这个特性使得程序结构更加精简。scala内建的控制结构很少,仅有if、while、for、try、match和函数调用等而已。如此之少的理由是,scala从语法层面上支持函数字面量
2.1 if else表达式
在scala中if/else表达式有值,这个值就是跟在if/esle后边的表达式的值。如果if或者else返回的类型不一样,就返回Any类型(所有类型的公共超类型)。scala中没有三目运算符,因为根本不需要。如下:
val s = if(x > 0) 1 else -1
例:if else返回类型一样
val a3 = 10
val a4 =
if(a3 > 20){
“a3大于20”
}else{
“a3小于20”
}
println(a4)
例如:if else返回类型不一样
val a5 =
if(a3 > 20){
“a3大于20”
}
println(a5)
说明:
如果缺少一个判断,什么都没有返回,但是Scala认为任何表达式都会有值,对于空值,使用Unit类,写做(),叫做无用占位符,相当于java中的void。
尖叫提示:
行尾的位置不需要分号,只要能够从上下文判断出语句的终止即可。但是如果在单行中写多个语句,则需要分号分割。scala的每个表达式都有一个类型
2.2 while表达式
Scala提供while和do循环,与If语句不同,While语句本身没有值,即整个While语句的结果是Unit类型的()。
while循环
var n = 1;
val while1 = while(n <= 10){
n += 1
}
println(while1)
println(n)
while循环的中断
import scala.util.control.Breaks
val loop = new Breaks
loop.breakable{
while(n <= 20){
n += 1;
if(n == 18){
loop.break()
}
}
}
println(n)
尖叫提示:
scala并没有提供break和continue语句来退出循环,如果需要break,可以通过几种方法来做:
1、使用Boolean型的控制变量
2、使用嵌套函数,从函数中return
3、使用Breaks对象的break方法。
2.3 for表达式
scala中没有类似于for(; ; )的for循环,你可以使用如下形式的for循环语句:
for(i <- 表达式)
该for表达式语法对于数组和所有集合类均有效。Scala 也为for 循环这一常见的控制结构提供了非常多的特性,这些for 循环的特性被称为for 推导式(for comprehension)或for 表达式(for expression)。
1)枚举:
for(i <- 1 to 10),其中“i <- 表达式”语法称之为发生器,该语句是让变量i(注意此处循环变量i是val的(但无需你指定),该变量的类型是集合的元素类型)遍历表达式中的所有值。1 to 10产生的Range包含上边界,如果不想包含上边界,可以使用until。
2)过滤:
也叫守卫,在for表达式的发生器中使用过滤器可以通过添加if子句实现,如:for(i <- 1 to 10 if i!=5),如果要添加多个过滤器,即多个if子句的话,要用分号隔开,如:for(i <- 1 to 10 if i!=5; if i!=6)。
3)嵌套枚举:
如果使用多个“<-”子句,你就得到了嵌套的“循环”,如:for(i <- 1 to 5; j <- 1 to i)。
4)流间变量绑定:
你可以在for发生器以及过滤器等中使用变量保存计算结果,以便在循环体中使用,从而避免多次计算以得到该结果。流间变量绑定和普通变量定义相似,它被当作val,但是无需声明val关键字。
5)制造新集合:
for(…) yield 变量/循环体,最终将产生一个集合对象,集合对象的类型与它第一个发生器的类型是兼容的。
6)实际:
for表达式具有等价于组合应用map、flatMap、filter和foreach这几种高阶函数的表达能力。实际上,所有的能够yield(产生)结果的for表达式都会被编译器转译为高阶方法map、flatMap及filter的组合调用;所有的不带yield的for循环都会被转译为仅对高阶函数filter和foreach的调用。正是由于这几个高阶函数支持了for表达式,所以如果一个数据类型要支持for表达式,它就要定义这几个高阶函数。有些时候,你可以使用for表达式代替map、flatMap、filter和foreach的显式组合应用,或许这样会更清晰明了呢。
for示例1:to左右两边为前闭后闭的访问
for(i <- 1 to 3; j <- 1 to 3){
print(i * j + " ")
}
for示例2:until左右两边为前闭后开的访问
for(i <- 1 until 3; j <- 1 until 3) {
print(i * j + " ")
}
for示例3:引入保护式(也称条件判断式)。保护式满足为true则进入循环内部,满足为false则跳过,类似于continue
for(i <- 1 to 3 if i != 2) {
print(i + " ")
}
for示例4:引入变量
for(i <- 1 to 3; j = 4 - i) {
print(j + " ")
}
for示例5:将遍历过程中处理的结果返回到一个,使用yield关键字
val for5 = for(i <- 1 to 10) yield i
println(for5)
for示例6:使用花括号{}代替小括号()
for{
i <- 1 to 3
j = 4 - i}
print(i * j + " ")
println()
尖叫提示:{}和()对于for表达式来说都可以。for 推导式有一个不成文的约定:当for 推导式仅包含单一表达式时使用原括号,当其包含多个表达式时使用大括号。值得注意的是,使用原括号时,早前版本的Scala 要求表达式之间必须使用分号。
2)函数定义的等号问题:
1.当定义函数且显式声明函数的返回类型时,必须有“=”。
2.当定义函数且未声明函数的返回类型时,如果函数的返回类型为 Unit,“=”可以省略;
3.如果人为地省略“=”,无论函数体的内容是什么,都认为返回类型是 Unit, 即函数只有副作用,且必须有{ };
4.如果有“=”,则进行自动类型推断。
5.函数体定义时有“=”时,如果函数仅计算单个结果表达式,则可以省略花括号
3)函数有无空括号的区别:
1.没有参数的函数可以加括号也可以不加括号。原则上,无副作用(返回有用值)的函数省略括号,有副作用(不返回有用值,一般是 Unit)的函数添加括号(提醒使用者需要额外小心)。
2.有括号的函数被调用时可以加括号也可以不加括号,不带括号的函数被调用时一定不能
加括号。
4)函数返回值问题:
定义函数时,除了递归函数之外,你可以省略返回值类型声明,scala会根据=号后边的表达式的类型推断返回值类型,同时=号后边表达式的值就是函数的返回值,你无需使用return语句(scala推荐你使用表达式值代替return返回值,当然根据你的需要,也可以显式使用return返回值)。示例如下:
def abs(x: Double) = if(x >= 0) x else -x
def fac(n: Int) = {
var r = 1
for(i <- 1 to n) r = r * i
r
}
尖叫提示:声明函数返回类型,总是有好处的,它可以使你的函数接口清晰。因此建议不要省略函数返回类型声明。
5)函数参数:
一般情况下,scala编译器是无法推断函数的参数类型的,因此你需要在参数列表中声明参数的类型。对于函数字面量来说,根据其使用环境的不同,scala有时可以推断出其参数类型。
scala里函数参数的一个重要特征是它们都是val(这是无需声明的,在参数列表里你不能显式地声明参数变量为val),不是var,所以你不能在函数里面给参数变量重新赋值,这将遭到编译器的强烈反对。
6)重复参数:
在scala中,你可以指明函数的最后一个参数是重复的,从而允许客户向函数传入可变长度参数列表。要想标注一个重复参数,可在参数的类型之后放一个星号“”。例如:
def echo(args: String) = for(arg <- args) println(arg)
这样的话,echo就可以被零至多个String参数调用。在函数内部,重复参数的类型是声明参数类型的数组。因此,echo函数里被声明为类型“String*”的args的类型实际上是Array[String]。然而,如果你有一个合适类型的数组,并尝试把它当作重复参数传入,会出现编译错误。要实现这个做法,你需要在数组名后添加一个冒号和一个_符号,以告诉编译器把数组中的每个元素当作参数,而不是将整个数组当作单一的参数传递给echo函数,如下:
echo(arr: _)
7)默认参数与命名参数:
函数的默认参数与java以及c++中相似,都是从左向右结合。另外,你也可以在调用时指定参数名。示例如下:
def fun(str: String, left: String = “[”, right: String = “]”) = left + str + right
fun(“hello”)
fun(“hello”, “<<<”)
fun(“hello”, left = “<<<”)
8)传名参数:
对于如下代码,myAssert带有一个函数参数,该参数变量的类型为不带函数参数的函数类型:
myAssert(predicate: () => Boolean) = {
if(!predicate())
throw new AssertionError
}
在使用时,我们需要使用如下的语法:
myAssert(() => 5 > 3)
这样很麻烦,我们可以使用如下称之为“传名参数”的语法简化之:
myAssert(predicate: => Boolean) = {
if(!predicate)
throw new AssertionError
}
以上代码在定义参数类型时是以“=>”开头而不是“() =>”,并在调用函数(通过函数类型的变量)时,不带“()”。现在你就可以这样使用了:
myAssert(5 > 3)
其中,“predicate: => Boolean”说明predicate是函数类型,在使用时传入的是函数字面量。注意与“predicate: Boolean”的不同,后者predicate是Boolean类型的(表达式)。
9)函数与操作符:
从技术层面上来说,scala没有操作符重载,因为它根本没有传统意义上的操作符。诸如“+”、“-”、“*”、“/”这样的操作符,其实调用的是方法。方法被当作操作符使用时,根据使用方式的不同,可以分为:中缀标注(操作符)、前缀标注、后缀标注。
中缀标注:中缀操作符左右分别有一个操作数。方法若只有一个参数(实际上是两个参数,因为有一个隐式的this),调用的时候就可以省略点及括号。实际上,如果方法有多个显式参数,也可以这样做,只不过你需要把参数用小括号全部括起来。如果方法被当作中缀操作符来使用(也即省略了点及括号),那么左操作数是方法的调用者,除非方法名以冒号“:”结尾(此时,方法被右操作数调用)。另外,scala的中缀标注不仅可以在操作符中存在,也可以在模式匹配、类型声明中存在,参见相应部分。
前缀标注:前缀操作符只有右边一个操作数。但是对应的方法名应该在操作符字符上加上前缀“unary_”。标识符中能作为前缀操作符用的只有+、-、!和~。
后缀标注:后缀操作符只有左边一个操作数。任何不带显式参数的方法都可以作为后缀操作符。
10)嵌套定义的函数:
嵌套定义的函数也叫本地函数,本地函数仅在包含它的代码块中可见。
11)函数字面量:
在scala中,你不仅可以定义和调用函数,还可以把它们写成匿名的字面量,也即函数字面量,并把它们作为值传递。函数字面量被编译进类,并在运行期间实例化为函数值(任何函数值都是某个扩展了scala包的若干FunctionN特质之一的类的实例,如Function0是没有参数的函数,Function1是有一个参数的函数等等。每一个FunctionN特质有一个apply方法用来调用函数)。因此函数字面量和值的区别在于函数字面量存在于源代码中,而函数值作为对象存在于运行期。这个区别很像类(源代码)和对象(运行期)之间的关系。
以下是对给定数执行加一操作的函数字面量:
(x: Int) => x + 1
其中,=>指出这个函数把左边的东西转变为右边的东西。在=>右边,你也可以使用{}来包含代码块。
函数值是对象,因此你可以将其存入变量中,这些变量也是函数,你可以使用通常的括号函数调用写法调用它们。如:
val fun = (x: Int) => x + 1
val a = fun(5)
有时,scala编译器可以推断出函数字面量的参数类型,因此你可以省略参数类型,然后你也可以省略参数外边的括号。如:
(x) => x + 1
x => x + 1
如果想让函数字面量更简洁,可以把通配符“”当作单个参数的占位符。如果遇见编译器无法识别参数类型时,在“”之后加上参数类型声明即可。如:
List(1,2,3,4,5).filter(_ > 3)
val fun = (: Int) + (: Int)
12)部分应用函数:
你还可以使用单个“”替换整个参数列表。例如可以写成:
List(1,2,3,4,5).foreach(println())
或者更好的方法是你还可以写成:
List(1,2,3,4,5).foreach(println _)
以这种方式使用下划线时,你就正在写一个部分应用函数。部分应用函数是一种表达式,你不需要提供函数需要的所有参数,代之以仅提供部分,或不提供所需参数。如下先定义一个函数,然后创建一个部分应用函数,并保存于变量,然后该变量就可以作为函数使用:
def sum(a: Int, b: Int, c: Int) = a + b + c
val a = sum _
println(a(1,2,3))
实际发生的事情是这样的:名为a的变量指向一个函数值对象,这个函数值是由scala编译器依照部分应用函数表达式sum _,自动产生的类的一个实例。编译器产生的类有一个apply方法带有3个参数(之所以带3个参数是因为sum _表达式缺少的参数数量为3),然后scala编译器把表达式a(1,2,3)翻译成对函数值的apply方法的调用。你可以使用这种方式把成员函数和本地函数转换为函数值,进而在函数中使用它们。不过,你还可以通过提供某些但不是全部需要的参数表达一个部分应用函数。如下,此变量在使用的时候,可以仅提供一个参数:
val b = sum(1, _: Int, 3)
如果你正在写一个省略所有参数的部分应用函数表达式,如println _或sum _,而且在代码的那个地方正需要一个函数,你就可以省略掉下划线(不是需要函数的地方,你这样写,编译器可能会把它当作一个函数调用,因为在scala中,调用无副作用的函数时,默认不加括号)。如下代码就是:
List(1,2,3,4,5).foreach(println)
13)偏函数
偏函数和部分应用函数是无关的。偏函数,它只对会作用于指定类型的参数或指定范围值的参数实施计算。偏函数是只对函数定义域的一个子集进行定义的函数。 scala中用scala.PartialFunction[-T, +S]来表示。偏函数主要用于这样一种场景:对某些值现在还无法给出具体的操作(即需求还不明朗),也有可能存在几种处理方式(视乎具体的需求),我们可以先对需求明确的部分进行定义,以后可以再对定义域进行修改。PartialFunction中可以使用的方法如下:
isDefinedAt:判断定义域是否包含指定的输入。
orElse:补充对其他域的定义。
compose:组合其他函数形成一个新的函数,假设有两个函数f和g,那么表达式f _ compose g _则会形成一个f(g(x))形式的新函数。你可以使用该方法对定义域进行一定的偏移。
andThen:将两个相关的偏函数串接起来,调用顺序是先调用第一个函数,然后调用第二个,假设有两个函数f和g,那么表达式f _ andThen g _则会形成一个g(f(x))形式的新函数,刚好与compose相反。
val f: PartialFunction[Char, Int] = {
case ‘+’ => 1
case ‘-’ => -1
}
println(f(’-’))
println(f.isDefinedAt(‘0’))
println(f(’+’))
// println(f(‘0’))
再深入探讨一点点:
我们定义一个将List集合里面数据+1的偏函数:
val f1 = new PartialFunction[Any, Int] {
def apply(any: Any) = any.asInstanceOf[Int] + 1
def isDefinedAt(any: Any) = if (any.isInstanceOf[Int]) true else false
}
val rf1 = List(1, 3, 5, “seven”) collect f1
println(rf1)
如上的功能,等同于:
def f2: PartialFunction[Any, Int] = {
case i: Int => i + 1
}
val rf2 = List(1, 3, 5, “seven”) collect f2
println(rf2)
14)函数示例
函数示例1:返回Unit类型的函数
def shout1(content: String) : Unit = {
println(content)
}
函数示例2:返回Unit类型的函数,但是没有显式指定返回类型。(当然也可以返回非Unit类型的值)
def shout2(content: String) = {
println(content)
}
函数示例3:返回值类型有多种可能,此时也可以省略Unit
def shout3(content: String) = {
if(content.length >= 3)
content + “喵喵喵~”
else
3
}
函数示例4:带有默认值参数的函数,调用该函数时,可以只给无默认值的参数传递值,也可以都传递,新值会覆盖默认值;传递参数时如果不按照定义顺序,则可以通过参数名来指定。
def shout4(content: String, leg: Int = 4) = {
println(content + “,” + leg)
}
函数示例5:变长参数(不确定个数参数)
def sum(args: Int*) = {
var result = 0
for(arg <- args)
result += arg
result
}
递归函数:递归函数在使用时必须有明确的返回值类型
def factorial(n: Int): Int = {
if(n <= 0)
n
else
n * factorial(n - 1)
}
尖叫提示:
1、Scala可以通过=右边的表达式 推断出函数的返回类型。如果函数体需要多个表达式,可以用代码块{}。
2、可以把return当做 函数版本的break语句。
3、递归函数一定要指定返回类型。
4、变长参数通过*来指定,所有参数会转化为一个seq序列。
3.2 过程
我们将函数的返回类型为Unit的函数称之为过程。
定义过程示例1:
def shout1(content: String) : Unit = {
println(content)
}
定义过程示例2:
def shout1(content: String) = {
println(content)
}
定义过程示例3:
def shout1(content: String) {
println(content)
}
尖叫提示:
这只是一个逻辑上的细分,如果因为该概念导致了理解上的混淆,可以暂时直接跳过过程这样的描述。毕竟过程,在某种意义上也是函数。
3.3 异常
当碰到异常情况时,方法抛出一个异常,终止方法本身的执行,异常传递到其调用者,调用者可以处理该异常,也可以升级到它的调用者。运行系统会一直这样升级异常,直到有调用者能处理它。 如果一直没有处理,则终止整个程序。
Scala的异常的工作机制和Java一样,但是Scala没有“checked”异常,你不需要声明说函数或者方法可能会抛出某种异常。受检异常在编译器被检查,java必须声明方法所会抛出的异常类型。
抛出异常:用throw关键字,抛出一个异常对象。所有异常都是Throwable的子类型。throw表达式是有类型的,就是Nothing,因为Nothing是所有类型的子类型,所以throw表达式可以用在需要类型的地方。
捕捉异常:在Scala里,借用了模式匹配的思想来做异常的匹配,因此,在catch的代码里,是一系列case字句。
异常捕捉的机制与其他语言中一样,如果有异常发生,catch字句是按次序捕捉的。因此,在catch字句中,越具体的异常越要靠前,越普遍的异常越靠后。 如果抛出的异常不在catch字句中,该异常则无法处理,会被升级到调用者处。
finally字句用于执行不管是正常处理还是有异常发生时都需要执行的步骤,一般用于对象的清理工作。
异常示例:
object ExceptionSyllabus {
def divider(x: Int, y: Int): Float= {
if(y == 0) throw new Exception(“0作为了除数”)
else x / y
}
def main(args: Array[String]): Unit = {
try {
println(divider(10, 3))
} catch {
case ex: Exception => println(“捕获了异常:” + ex)
} finally {}
}
}
1)不可变集合继承层次:
2)可变集合继承层次:
4.2 数组 Array
在scala中,数组保存相同类型的元素,其中包含的元素值是可变的。数组也是对象,访问数组使用小括号。在JVM中,scala的数组以java数组方式实现。scala中数组是非协变的。
定长数组使用Array,创建之后长度不可改变。变长数组使用ArrayBuffer。
与java一样,scala中多维数组也是通过数组的数组来实现的。构造多维数组可以使用ofDim方法或者直接使用for循环来new。示例如下:
val matrix = Array.ofDimDouble // ofDim方法创建多维数组
matrix(1)(2) = 12.36
val mutliarr = new ArrayArray[Int] // for循环方式创建多维数组
for(i <- 0 until mutliarr.length)
mutliarr(i) = new ArrayInt
定长数组
//定义
val arr1 = new ArrayInt
//赋值
arr1(1) = 7
或:
//定义
val arr1 = Array(1, 2)
变长数组
//定义
val arr2 = ArrayBufferInt
//追加值
arr2.append(7)
//重新赋值
arr2(0) = 7
定长数组与变长数组的转换
arr1.toBuffer
arr2.toArray
多维数组
//定义
val arr3 = Array.ofDimDouble
//赋值
arr3(1)(1) = 11.11
与Java数组的互转
Scal数组转Java数组:
val arr4 = ArrayBuffer(“1”, “2”, “3”)
//Scala to Java
import scala.collection.JavaConversions.bufferAsJavaList
val javaArr = new ProcessBuilder(arr4)
println(javaArr.command())
Java数组转Scala数组:
import scala.collection.JavaConversions.asScalaBuffer
import scala.collection.mutable.Buffer
val scalaArr: Buffer[String] = javaArr.command()
println(scalaArr)
4.3 元组 Tuple
元组Tuple也是不可变的,但元组可以包含不同类型的元素,并且因此而不能继承自Iterable。
元组实例化之后,可以使用点号、下划线和从1开始的索引访问其中的元素。因为元组可以保存不同类型的元素,所以不能使用apply方法访问其元素(apply返回同样的类型)。元组的索引从1开始,是因为对于拥有静态类型元组的其他语言,如Haskell和ML,从1开始是传统的设定。
scala的任何对象都可以调用“->”方法,并返回包含键值对的二元组(也叫对偶,是元组的最简单形态),比如 “hello” -> 100 则创建出 (“hello”, 100)。
元组也是可以理解为一个容器,可以存放各种相同或不同类型的数据。
元组的创建
val tuple1 = (1, 2, 3, “heiheihei”)
println(tuple1)
元组数据的访问,注意元组元素的访问有下划线,并且访问下标从1开始,而不是0
val value1 = tuple1._4
println(value1)
元组的遍历
方式1:
for (elem <- tuple1.productIterator) {
print(elem)
}
println()
方式2:
tuple1.productIterator.foreach(i => println(i))
tuple1.productIterator.foreach(print(_))
4.4 列表 List
列表保存相同类型的元素。scala里的列表类型是协变的,这意味着如果S是T的子类,那么List[S]也是List[T]的子类。
不可变列表使用List,一旦创建之后就不可改变。可变列表使用ListBuffer。
List是抽象类,它有两个子类型:Nil和::。Nil是空列表对象,类型是List[Nothing]。::是样本类,可以创建非空列表,::的伴生对象可以以中缀标注的形式用于模式匹配。所以在scala中存在两个::,一个是样本类,另一个是List的方法,因此在构造一个列表时,我们就有了多种方法。
List类没有提供append操作(向列表尾部追加),因为随着列表变长,效率将逐渐低下。List提供了“::”做前缀插入,因为这将消耗固定时间。如果你想通过添加元素来构造列表,你的选择是先把它们前缀插入,完成之后再调用reverse;或者使用ListBuffer,一种提供append操作的可变列表,完成之后调用toList。
创建List
val list1 = List(1, 2)
println(list1)
访问List元素
val value1 = list1(1)
println(value1)
List元素的追加
val list2 = list1 :+ 99
println(list2)
val list3 = 100 +: list1
println(list3)
List的创建与追加,符号“::”,注意观察去掉Nil和不去掉Nil的区别
val list4 = 1 :: 2 :: 3 :: list1 :: Nil
println(list4)
4.5 队列 Queue
队列数据存取符合先进先出策略
队列的创建
import scala.collection.mutable
val q1 = new mutable.Queue[Int]
println(q1)
队列元素的追加
q1 += 1;
println(q1)
向队列中追加List
q1 ++= List(2, 3, 4)
println(q1)
按照进入队列的顺序删除元素
q1.dequeue()
println(q1)
塞入数据
q1.enqueue(9, 8, 7)
println(q1)
返回队列的第一个元素
println(q1.head)
返回队列最后一个元素
println(q1.last)
返回除了第一个以外的元素
println(q1.tail)
4.6 映射 Map
这个地方的学习,就类比Java的map集合学习即可。
构造不可变映射
val map1 = Map(“Alice” -> 10, “Bob” -> 20, “Kotlin” -> 30)
构造可变映射
val map2 = scala.collection.mutable.Map(“Alice” -> 10, “Bob” -> 20, “Kotlin” -> 30)
空的映射
val map3 = new scala.collection.mutable.HashMap[String, Int]
对偶元组
val map4 = Map((“Alice”, 10), (“Bob”, 20), (“Kotlin”, 30))
取值
如果映射中没有值,则会抛出异常,使用contains方法检查是否存在key。如果通过映射.get(键) 这样的调用返回一个Option对象,要么是Some,要么是None。
val value1 = map1(“Alice”)//建议使用get方法得到map中的元素
println(value1)
更新值
map2(“Alice”) = 99
println(map2(“Alice”))
或:
map2 += (“Bob” -> 99)
map2 -= (“Alice”, “Kotlin”)
println(map2)
或:
val map5 = map2 + (“AAA” -> 10, “BBB” -> 20)
println(map5)
遍历
for ((k, v) <- map1) println(k + " is mapped to " + v)
for (v <- map1.keys) println(v)
for (v <- map1.values) println(v)
for(v <- map1) prinln(v)
4.7 集 Set
集是不重复元素的结合。集不保留顺序,默认是以哈希集实现。如果想要按照已排序的顺序来访问集中的元素,可以使用SortedSet(已排序数据集),已排序的数据集是用红黑树实现的。默认情况下,Scala 使用的是不可变集合,如果你想使用可变集合,需要引用 scala.collection.mutable.Set 包。
Set不可变集合的创建
val set = Set(1, 2, 3)
println(set)
Set可变集合的创建,如果import了可变集合,那么后续使用默认也是可变集合
import scala.collection.mutable.Set
val mutableSet = Set(1, 2, 3)
可变集合的元素添加
mutableSet.add(4)
mutableSet += 6
// 注意该方法返回一个新的Set集合,而非在原有的基础上进行添加
mutableSet.+(5)
可变集合的元素删除
mutableSet -= 1
mutableSet.remove(2)
println(mutableSet)
遍历
for(x <- mutableSet) {
println(x)
}
Set更多常用操作
1 def +(elem: A): Set[A] 为集合添加新元素,并创建一个新的集合,除非元素已存在
2 def -(elem: A): Set[A] 移除集合中的元素,并创建一个新的集合
3 def contains(elem: A): Boolean 如果元素在集合中存在,返回true,否则返回 false。
4 def &(that: Set[A]): Set[A] 返回两个集合的交集
5 def &~(that: Set[A]): Set[A] 返回两个集合的差集
6 def ++(elems: A): Set[A] 合并两个集合
7 def drop(n: Int): Set[A]] 返回丢弃前n个元素新集合
8 def dropRight(n: Int): Set[A] 返回丢弃最后n个元素新集合
9 def dropWhile(p: (A) => Boolean): Set[A] 从左向右丢弃元素,直到条件p不成立
10 def max: A 查找最大元素
11 def min: A 查找最小元素
12 def take(n: Int): Set[A] 返回前 n 个元素
4.8 集合元素与函数的映射
map:将集合中的每一个元素映射到某一个函数
val names = List(“Alice”, “Bob”, “Nick”)
println(names.map(_.toUpperCase))
flatmap:flat即压扁,压平,扁平化,效果就是将集合中的每个元素的子元素映射到某个函数并返回新的集合
val names = List(“Alice”, “Bob”, “Nick”)
println(names.flatMap(_.toUpperCase()))
4.9 化简、折叠、扫描
折叠,化简:将二元函数引用于集合中的函数
val list = List(1, 2, 3, 4, 5)
val i1 = list.reduceLeft(_ - )
val i2 = list.reduceRight( - )
println(i1)
println(i2)
.reduceLefft( - )这个函数的执行逻辑如图所示: (.reduceRight( - _)反之同理)
折叠,化简:fold
fold函数将上一步返回的值作为函数的第一个参数继续传递参与运算,直到list中的所有元素被遍历。可以把reduceLeft看做简化版的foldLeft。相关函数:fold,foldLeft,foldRight,可以参考reduce的相关方法理解。
val list2 = List(1, 9, 2, 8)
val i4 = list2.fold(5)((sum, y) => sum + y)
println(i4)
foldRight
val list3 = List(1, 9, 2, 8)
val i5 = list3.foldRight(100)(_ - )
println(i5)
尖叫提示:
foldLeft和foldRight有一种缩写方法对应分别是/:和:\
foldLeft
val list4 = List(1, 9, 2, 8)
val i6 = (0 /: list4)( - _)
println(i6)
统计一句话中,各个文字出现的次数
val sentence = “一首现代诗《笑里藏刀》:哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈刀哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈”//m + (“一” -> 1, “首” -> 1, “哈” -> 1)
val i7 = (MapChar, Int /: sentence)((m, c) => m + (c -> (m.getOrElse(c, 0) + 1)))
println(i7)
折叠,化简,扫描
这个理解需要结合上面的知识点,扫描,即对某个集合的所有元素做fold操作,但是会把产生的所有中间结果放置于一个集合中保存。
val i8 = (1 to 10).scanLeft(0)(_ + _)
println(i8)
4.10 zip拉链操作
zip方法让你将他们组合成一个对偶的列表例如 prices zip quanties
val prices = List(5.0,20.0,9.95)
val quantities = List(10,2,1)
将得到:List[(Double,Int)] = List{(5.0,10),(20.0,2),(9.95,1)}
方法之所以叫做“拉链”,是因为它就像拉链的齿状结构一样将两个集合结合在一起。
这样一来对每个对偶应用函数就很容易了。
(prices zip quantities) map{ p=> p._1p._2}
将得到:List(50.0,40.0,9.95)
所有物件的总价就是:((prices zip quantities) map(p=>p._1p._2)) sum
4.11 迭代器
Iterable是可变和不可变序列、集、映射的超特质。集合对象可以通过调用iterator方法来产生迭代器Iterator。Iterable与Iterator之间的差异在于:前者指代的是可以被枚举的类型,而后者是用来执行枚举操作的机制。尽管Iterable可以被枚举若干次,但Iterator仅能使用一次。
可以通过iterator方法从集合获得一个迭代器,通过while循环和for表达式对集合进行遍历。
val iterator = List(1, 2, 3, 4, 5).iterator
while (iterator.hasNext) {
println(iterator.next())
}
或:
for(enum <- iterator) {
println(enum)
}
4.12 流 Stream
stream是一个集合。这个集合,可以用于存放,无穷多个元素,但是这无穷个元素并不会一次性生产出来,而是需要用到多大的区间,就会动态的生产,末尾元素遵循lazy规则。
使用#::得到一个stream
def numsForm(n: BigInt) : Stream[BigInt] = n #:: numsForm(n + 1)
传递一个值,并打印stream集合
val tenOrMore = numsForm(10)
println(tenOrMore)
tail的每一次使用,都会动态的向stream集合按照规则生成新的元素
println(tenOrMore.tail)
println(tenOrMore)
使用map映射stream的元素并进行一些计算
println(numsForm(5).map(x => x * x))
4.13 视图 View
Stream的懒执行行为,你可以对其他集合应用view方法来得到类似的效果,该方法产出一个其方法总是被懒执行的集合。但是view不会缓存数据,每次都要重新计算。
例如:我们找到10万以内,所有数字倒序排列还是它本身的数字。
val viewSquares = (1 to 100000)
.view
.map(x => {
// println(x)
x.toLong * x.toLong
}).filter(x => {
x.toString == x.toString.reverse
})
println(viewSquares(3))
for(x <- viewSquares){
print(x + “,”)
}
4.14 线程安全的集合
所有线程安全的集合都是以Synchronized开头的集合,例如:
SynchronizedBuffer
SynchronizedMap
SynchronizedPriorityQueue
SynchronizedQueue
SynchronizedSet
SynchronizedStack
4.15 并行集合
Scala为了充分使用多核CPU,提供了并行集合(有别于前面的串行集合),用于多核环境的并行计算。
主要用到的算法有:
1)Divide and conquer : 分治算法,Scala通过splitters,combiners等抽象层来实现,主要原理是将计算工作分解很多任务,分发给一些处理器去完成,并将它们处理结果合并返回
2)Work stealin:算法,主要用于任务调度负载均衡(load-balancing),通俗点完成自己的所有任务之后,发现其他人还有活没干完,主动(或被安排)帮他人一起干,这样达到尽早干完的目的。
打印1~5
(1 to 5).foreach(println())
println()
(1 to 5).par.foreach(println())
查看并行集合中元素访问的线程
val result1 = (0 to 10000).map{case _ => Thread.currentThread.getName}.distinct
val result2 = (0 to 10000).par.map{case _ => Thread.currentThread.getName}.distinct
println(result1)
println(result2)
4.16 操作符
这部分内容没有必要刻意去理解和记忆,语法使用的多了,自然就会产生感觉,该部分内容暂时大致了解一下即可。
val
= 425.4 模式中的变量
如果在case关键字后跟变量名,那么match前表达式的值会赋给那个变量。
val str = “±3!”
for (i <- str.indices) {
var sign = 0
var digit = 0
str(i) match {
case ‘+’ => sign = 1
case ‘-’ => sign = -1
case ch if Character.isDigit(ch) => digit = Character.digit(ch, 10)
case _ =>
}
println(str(i) + " " + sign + " " + digit)
}
5.5 类型模式
可以匹配对象的任意类型,但是不能直接匹配泛型类型,这样描述比较抽象,看下面的例子:这样做的意义在于,避免了使用isInstanceOf和asInstanceOf方法。
val a = 8
val obj = if(a == 1) 1
else if(a == 2) “2”
else if(a == 3) BigInt(3)
else if(a == 4) Map(“aa” -> 1)
else if(a == 5) Map(1 -> “aa”)
else if(a == 6) Array(1, 2, 3)
else if(a == 7) Array(“aa”, 1)
else if(a == 8) Array(“aa”)
val r1 = obj match {
case x: Int => x
case s: String => s.toInt
case BigInt => -1 //不能这么匹配
case : BigInt => Int.MaxValue
case m: Map[String, Int] => “Map[String, Int]类型的Map集合”
case m: Map[, ] => “Map集合”
case a: Array[Int] => “It’s an Array[Int]”
case a: Array[String] => “It’s an Array[String]”
case a: Array[] => “It’s an array of something other than Int”
case _ => 0
}
println(r1 + ", " + r1.getClass.getName)
尖叫提示:Map类型的泛型在匹配的时候,会自动删除泛型类型,只会匹配到Map类型,而不会精确到Map里面的泛型类型。
5.6 匹配数组、列表、元组
Array(0) 匹配只有一个元素且为0的数组。
Array(x,y) 匹配数组有两个元素,并将两个元素赋值为x和y。
Array(0,_*) 匹配数组以0开始。
匹配数组
for (arr <- Array(Array(0), Array(1, 0), Array(0, 1, 0), Array(1, 1, 0), Array(1, 1, 0, 1))) {
val result = arr match {
case Array(0) => “0”
case Array(x, y) => x + " " + y
case Array(x, y, z) => x + " " + y + " " + z
case Array(0, _*) => “0…”
case _ => “something else”
}
println(result)
}
匹配列表
与匹配数组相似,同样可以应用于列表
for (lst <- Array(List(0), List(1, 0), List(0, 0, 0), List(1, 0, 0))) {
val result = lst match {
case 0 :: Nil => “0”
case x :: y :: Nil => x + " " + y
case 0 :: tail => “0 …”
case _ => “something else”
}
println(result)
}
匹配元组
同样可以应用于元组
for (pair <- Array((0, 1), (1, 0), (1, 1))) {
val result = pair match {
case (0, _) => “0 …”
case (y, 0) => y + " 0"
case _ => “neither is 0”
}
println(result)
}
5.7 提取器
模式匹配,什么才算是匹配呢?即,case中unapply方法返回some集合则为匹配成功,返回none集合则为匹配失败。下面我们来看几个例子做详细探讨。
unapply
—— 调用unapply,传入number
—— 接收返回值并判断返回值是None,还是Some
—— 如果是Some,则将其解开,并将其中的值赋值给n(就是case Square(n)中的n)
创建object Square:
object Square {
def unapply(z: Double): Option[Double] = Some(math.sqrt(z))
}
模式匹配使用:
val number: Double = 36.0
number match {
case Square(n) => println(s"square root of $number is $n")
case _ => println(“nothing matched”)
模式匹配使用:
val namesString = “Alice,Bob,Thomas”
namesString match {
case Names(first, second, third) => {
println(“the string contains three people’s names”)
println(s"$first $second $third")
}
case _ => println(“nothing matched”)
}
5.8 模式无处不在
1)变量声明中的模式
match中每一个case都可以单独提取出来,意思是一样的,如下:
val (x, y) = (1, 2)
val (q, r) = BigInt(10) /% 3
val arr = Array(1, 7, 2, 9)
val Array(first, second, *) = arr
println(first, second)
2)for表达式中的模式
import scala.collection.JavaConverters.
for ((k, v) <- System.getProperties.asScala)
println(k + " -> " + v)
for ((k, “”) <- System.getProperties.asScala)
println(k)
for ((k, v) <- System.getProperties.asScala if v == “”)
println(k)
尖叫提示:for中匹配会自动忽略失败的匹配
3) 模式匹配花括号中的样本序列(即备选项)可以用在能够出现函数字面量的任何地方,实质上,样本序列就是更普遍的函数字面量,函数字面量只有一个入口点和参数列表,样本序列可以有多个入口点,每个都有自己的参数列表,每个样本都是函数的一个入口点,参数被模式所特化。如下:
val withDefault: Option[Int] => String = {
case Some(x) => “is int”
case None => “?”
}
5.9 样例类
样例类首先是类,除此之外它是为模式匹配而优化的类,样例类用case关键字进行声明:
样例类的创建
package unit6
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的对象时,我们可以用模式匹配来匹配他的类型,并将属性值绑定到变量:
for (amt <- Array(Dollar(1000.0), Currency(1000.0, “EUR”), Nothing)) {
val result = amt match {
case Dollar(v) => “$” + v
case Currency(_, u) => u
case Nothing => “”
}
println(amt + ": " + result)
}
当你声明样例类时,如下几件事情会自动发生:
—— 构造其中的每一个参数都成为val——除非它被显式地声明为var(不建议这样做)
—— 在半生对象中提供apply方法让你不用new关键字就能构造出相应的对象,比如Dollar(29.95)或Currency(29.95, “EUR”)
—— 提供unapply方法让模式匹配可以工作
—— 将生成toString、equals、hashCode和copy方法——除非显式地给出这些方法的定义。
除上述外,样例类和其他类型完全一样。你可以添加方法和字段,扩展它们。
5.10 Copy方法和带名参数
copy创建一个与现有对象值相同的新对象,并可以通过带名参数来修改某些属性。
val amt = Currency(29.95, “EUR”)
val price = amt.copy(value = 19.95)
println(amt)
println(price)
println(amt.copy(unit = “CHF”))
5.11 Case语句的中置(缀)表达式
什么是中置表达式?1 + 2,这就是一个中置表达式。如果unapply方法产出一个元组,你可以在case语句中使用中置表示法。比如可以匹配一个List序列。
List(1, 7, 2, 9) match {
case first :: second :: rest => println(first + second + rest.length)
case _ => 0
}
5.12 匹配嵌套结构
比如某一系列商品想捆绑打折出售
创建样例类
abstract class Item
case class Article(description: String, price: Double) extends Item
case class Bundle(description: String, discount: Double, item: Item*) extends Item
匹配嵌套结构
val sale = Bundle(“愚人节大甩卖系列”, 10,
Article("《九阴真经》", 40),
Bundle(“从出门一条狗到装备全发光的修炼之路系列”, 20,
Article("《如何快速捡起地上的装备》", 80),
Article("《名字起得太长躲在树后容易被地方发现》",30)))
将descr绑定到第一个Article的描述
val result1 = sale match {
case Bundle(_, _, Article(descr, _), _*) => descr}
println(result1)
通过@表示法将嵌套的值绑定到变量。*绑定剩余Item到rest
val result2 = sale match {
case Bundle(, , art @ Article(, _), rest @ _*) => (art, rest)
}
println(result2)
不使用_*绑定剩余Item到rest
val result3 = sale match {
case Bundle(_, , art @ Article(, _), rest) => (art, rest)
}
println(result3)
计算某个Item价格的函数,并调用
def price(it: Item): Double = {
it match {
case Article(, p) => p
case Bundle(, disc, its@_*) => its.map(price _).sum - disc
}
}
println(SwitchBaseSyllabus.price(sale))
5.13 模拟枚举
样例类可以模拟出枚举类型
package unit6
sealed abstract class TrafficLightColor
case object Red extends TrafficLightColor
case object Yellow extends TrafficLightColor
case object Green extends TrafficLightColor
尖叫提示:带有一个参数的函数的类型是function1,带有两个是function2,以此类推
6.2 匿名函数
即没有名字的函数,可以通过函数表达式来设置匿名函数。
val triple = (x: Double) => 3 * x
println(triple(3))
6.3 高阶函数
能够接受函数作为参数的函数,叫做高阶函数。
高阶函数的使用
def highOrderFunction1(f: Double => Double) = f(10)
def minus7(x: Double) = x - 7
val result2 = highOrderFunction1(minus7)
println(result2)
高阶函数同样可以返回函数类型
def minusxy(x: Int) = (y: Int) => x - y
val result3 = minusxy(3)(5)
println(result3)
6…4 参数(类型)推断
// 传入函数表达式
highOrderFunction1((x: Double) => 3 * x)
// 参数推断省去类型信息
highOrderFunction1((x) => 3 * x)
// 单个参数可以省去括号
highOrderFunction1(x => 3 * x)
// 如果变量旨在=>右边只出现一次,可以用_来代替
highOrderFunction1(3 * _)
6.5 闭包
闭包就是一个函数把外部的那些不属于自己的对象也包含(闭合)进来。
def minusxy(x: Int) = (y: Int) => x – y
这就是一个闭包:
匿名函数(y: Int) => x -y嵌套在minusxy函数中。
匿名函数(y: Int) => x -y使用了该匿名函数之外的变量x
函数minusxy返回了引用了局部变量的匿名函数
再举一例:
def minusxy(x: Int) = (y: Int) => x - y
val f1 = minusxy(10)
val f2 = minusxy(10)
println(f1(3) + f2(3))
此处f1,f2这两个函数就叫闭包。
尖叫提示:
在scala中,闭包捕获了变量本身,而不是变量的值。变量的变化在闭包中是可见的,反过来,若闭包改变对应变量的值,在外部也是可见的。
6.6 柯里化
柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。如下就是一个柯里化之后的函数:
def curriedSum(x: Int)(y: Int) = x + y
这里发生的事情是当你调用curriedSum时,实际上接连调用了两个传统函数。第一个调用的函数带单个名为x的参数,并返回第二个函数的函数值;这个被返回的函数带一个参数y,并返回最终计算结果。你可以使用部分应用函数表达式方式,来获取第一个调用返回的函数,也即第二个函数,如下:
val onePlus = curriedSum(3)_
柯里化就是以函数为主体这种思想发展的必然产生的结果。
柯里化示例
def mul(x: Int, y: Int) = x * y
println(mul(10, 10))
def mulCurry(x: Int) = (y: Int) => x * y
println(mulCurry(10)(9))
def mulCurry2(x: Int)(y:Int) = x * y
println(mulCurry2(10)(8))
其中corresponds函数的源码如下:
def corresponds[B](that: GenSeq[B])(p: (A,B) => Boolean): Boolean = {
val i = this.iterator
val j = that.iterator
while (i.hasNext && j.hasNext)
if (!p(i.next(), j.next()))
return false
!i.hasNext && !j.hasNext
}
尖叫提示:不要设立柯里化存在的意义这样的命题,柯里化,是面向函数思想的必然产生结果。
6.7 控制抽象
控制抽象是一类函数:
1、参数是函数。
2、函数参数没有输入值也没有返回值。
是不是很爽?是不是有点类似线程池的感觉,同一个线程,可以动态的向里面塞不同的任务去执行。
可以再简化一下,省略(),看下如下形式:
def runInThread(f1: => Unit): Unit = {
new Thread {
override def run(): Unit = {
f1
}
}.start()
}
runInThread {
println(“干活咯!”)
Thread.sleep(5000)
println(“干完咯!”)
}
尖叫提示:在Scala中,类并不声明为Public,一个Scala源文件可以包含多个类。所有这些类都具有公有可见性。调用无参方法时,可以加(),也可以不加;如果方法定义中不带括号,那么调用时就不能带括号。
7.2 访问级别控制
公有是scala的默认访问级别,因此如果你想使成员公有,就不要指定任何访问修饰符。公有的成员可以在任何地方被访问。
私有类似于java,即在之前加上private。不同的是,在scala中外部类不可以访问内部类的私有成员。
保护类似于java,即在之前加上protected。不同的是,在scala中同一个包中的其他类不能访问被保护的成员。
scala里的访问修饰符可以通过使用限定词强调。格式为private[X]或protected[X]的修饰符表示“直到X”的私有或保护,这里X指代某个所属的包、类或单例对象。
scala还有一种比private更严格的访问修饰符,即private[this]。被private[this]标记的定义仅能在包含了定义的同一个对象中被访问,这种限制被称为对象私有。这可以保证成员不被同一个类中的其他对象访问。
对于私有或者保护访问来说,scala的访问规则给予了伴生对象和类一些特权,伴生对象可以访问所有它的伴生类的私有成员、保护成员,反过来也成立。
7.3 Getter Setter方法
在scala中,类的每个非私有的var成员变量都隐含定义了getter和setter方法,但是它们的命名并没有沿袭java的约定,var变量x的getter方法命名为“x”,它的setter方法命名为“x_=”。你也可以在需要的时候,自行定义相应的getter和setter方法,此时你还可以不定义关联的字段,自行定义setter的好处之一就是你可以进行赋值的合法性检查。
如果你将scala字段标注为@BeanProperty时,scala编译器会自动额外添加符合JavaBeans规范的形如getXxx/setXxx的getter和setter方法。这样的话,就方便了java与scala的互操作。对于scala类中的每一个属性,编译后,会有一个私有的字段和相应的getter、setter方法生成:
//getter
println(dog leg)
//setter
dog.leg_=(10)
println(dog currentLeg)
当然了,你也可以不使用自动生成的方式,自己定义getter和setter方法
class Dog2 {
private var _leg = 4
def leg = leg
def leg=(newLeg: Int) {
_leg = newLeg
}
}
使用之:
val dog2 = new Dog2
dog2.leg_=(10)
println(dog2.leg)
尖叫提示:自己手动创建变量的getter和setter方法需要遵循以下原则:
字段属性名以“_”作为前缀,如:_leg
getter方法定义为:def leg = _leg
setter方法定义时,方法名为属性名去掉前缀,并加上后缀,后缀是:“leg_=”,如例子所示
7.4 构造器
scala中构造分为主构造器和辅助构造器,Scala不需要显式定义构造方法,而是把类内部非字段、非方法的代码都当作“主构造方法”。辅助构造器的定义形式为def this(…)。每个辅助构造器都以“this(…)”的形式开头以调用本类中的其他构造器,被调用的构造器可以是主构造器,也可以是源文件中早于调用构造器定义的其他辅助构造器。其结果是对scala构造器的调用终将导致对主构造器的调用,因此主构造器是类的唯一入口点。在scala中,只有主构造器可以调用超类的构造器。
1)可以在类参数列表之前加上private关键字,使类的主构造器私有,私有的主构造器只能被类本身以及伴生对象访问。
2)可以使用require方法来为构造器的参数加上先决条件,如果不满足要求的话,require会抛出异常,阻止对象的创建。
主构造的参数直接放置于类名之后
定义类:
class ClassConstructor (var name: String, private var price: Double){
def myPrintln = println(name + “,” + price)
}
执行:
val classConstructor = new ClassConstructor("《傲慢与偏见》", 20.5)
classConstructor.myPrintln
主构造器会执行类定义中的所有语句
定义类:
class ClassConstructor2(val name: String = “”, val price: Double = 0) {
println(name + “,” + price)
}
执行:
val classConstructor2 = new ClassConstructor2(“aa”, 20)
val classConstructor2_2 = new ClassConstructor2()
3) 通过private设置的主构造器的私有属性
可以参考1)
4) 如果不带val和var的参数至少被一个方法使用,该参数将自动升级为字段,这时,name和price就变成了类的不可变字段,而且这两个字段是对象私有的,这类似于 private[this] val 字段的效果。否则,该参数将不被保存为字段,即实例化该对象时传入的参数值,不会被保留在实例化后的对象之中。
5)如果想让主构造器变成私有的,可以在()之前加上private,这样用户只能通过辅助构造器来构造对象了
class Person private () {…}
7.10 Bean属性
JavaBeans规范定义了Java的属性是像getXXX()和setXXX()的方法。许多Java工具都依赖这个命名习惯。为了Java的互操作性。将Scala字段加@BeanProperty时,这样的方法会自动生成。
创建一个Bean,使用@BeanProperty注解标识某个属性变量
package unit7
import scala.beans.BeanProperty
class Person {
@BeanProperty var name: String = _
}
通过getName、setName访问属性
val person = new Person
person.setName(“Nick”)
person.getName
println(person.name)
尖叫提示:
Person将会生成四个方法:
—— name:String
—— name_=(newValue:String): Unit
—— getName():String
—– setName(newValue:String):Unit
7.11 工厂对象与工厂方法
如果定义一个方法专门用来构造某一个类的对象,那么这种方法就称为“工厂方法”。包含这些工厂方法集合的单例对象,也就叫“工厂对象” 。通常,工厂方法会定义在伴生对象里。尤其是当一系列类存在继承关系时,可以在基类的伴生对象里定义一系列对应的工厂方法。使用工厂方法的好处是可以不用直接使用new来实例化对象,改用方法调用,而且方法名可以是任意的,这样对外隐藏了类的实现细节。例如:
// students.scala
class Students(val name: String, var score: Int) {
def exam(s: Int) = score = s
override def toString = name + "'s score is " + score + “.”
}
object Students {
def registerStu(name: String, score: Int) = new Students(name, score)
}
将文件students.scala编译后,并在解释器里用“import Students.”导入单例对象后,就能这样使用:
scala> import Students.
import Students._
scala> val stu = registerStu(“Tim”, 100)
stu: Students = Tim’s score is 100.
val cat1 = new Cat
val cat2 = new Cat
cat1.changeName(“黑猫”)
cat2.changeName(“白猫”)
cat1.describe
cat2.describe
尖叫提示:类和它的伴生对象可以相互访问私有特性,他们必须存在同一个源文件中。必须同名
Apply方法:
有一个特殊的方法名——apply,如果定义了这个方法,那么既可以显式调用——“对象.apply(参数)”,也可以隐式调用——“对象(参数)”。隐式调用时,编译器会自动插入缺失的“.apply”。如果apply是无参方法,应该写出空括号,否则无法隐式调用。无论是类还是单例对象,都能定义这样的apply方法。
8.2 对象相等性
与java不同的是,在scala中,“”和“!=”可以直接用来比较对象的相等性,“”和“!=”方法会去调用equals方法,因此一般情况下你需要覆盖equals方法。如果要判断引用是否相等,可以使用eq和ne。
在使用具有哈希结构的容器类库时,我们需要同时覆盖hashCode和equals方法,但是实现一个正确的hashCode和equals方法是比较困难的一件事情,你需要考虑的问题和细节很多,可以参见java总结中的相应部分。另外,正如样本类部分所讲的那样,一旦一个类被声明为样本类,那么scala编译器就会自动添加正确的符合要求的hashCode和equals方法。
8.3 应用程序对象
每一个Scala应用程序都需要从一个对象的main方法开始执行,这个方法的类型为Array[String]=>Unit:
object Hello {
def main(args: Array[String]) {
println(“Hello, World!”)
}
}
或者扩展一个App特质:
object Hello extends App {
if (args.length > 0)
println("Hello, " + args(0))
else
println(“Hello, World!”)
}
8.4 枚举Enumeration类的对象
Scala中没有枚举类型,定义一个扩展Enumeration类的对象,并以value调用初始化枚举中的所有可能值:
object TrafficLightColor extends Enumeration {
val Red = Value(0, “Stop”)
val Yellow = Value(1, “Slow”)
val Green = Value(2, “Go”)
}
测试:
println(TrafficLightColor.Red)
println(TrafficLightColor.Red.id)
println(TrafficLightColor.Yellow)
println(TrafficLightColor.Yellow.id)
println(TrafficLightColor.Green)
println(TrafficLightColor.Green.id)
尖叫提示:位于文件顶部不带花括号的包声明在整个文件范围内有效。
通过以上形式,发出总结:
1、包也可以像内部类那样嵌套,作用域原则:可以直接向上访问。即,子package中直接访问父package中的内容。(即:作用域)
2、包对象(package object people)可以持有函数和变量,包中(package people)的类可以直接引用包对象中定义的方法和变量。
3、引入语句可以引入包、类和对象
4、源文件的目录和包之间并没有强制的关联关系
5、可以在同一个.scala文件中,声明多个并列的package
6、包名可以相对也可以绝对,比如,访问ArrayBuffer的绝对路径是:root.scala.collection.mutable.ArrayBuffer
9.2 包对象
包可以包含类、对象和特质,但不能包含函数或变量的定义。这是Java虚拟机的局限。包对象的出现正是为了解决这个局限。每个包都可以有一个包对象。你需要在父包中定义它,且名称与子包一样。包对象(package object people)可以持有函数和变量,包中(package people)的类可以直接引用包对象中定义的方法和变量。
package com.nick.impatient
package object people {
val defaultName = “Nick”
}
package people {
class Person {
var name = defaultName // 从包对象拿到的常置
}
}
9.3 包可见性
在Java中,没有被声明为public、private或protected的类成员在包含该类的包中可见。在Scala中,你可以通过修饰符达到同样的效果。以下方法在它自己的包中可见:
package com.nick.impatient.people
class Person {
private[people] def description=“人的名字:” + name
}
当然,也可以将可见度延展到上层包:
private[impatient] def description=“人的名字:” + name
9.4 引入
在Scala中,import语句可以出现在任何地方,并不仅限于文件顶部。import语句的效果一直延伸到包含该语句的块末尾:这是个很有用的特性,尤其是对于通配引入而言。从多个源引入大量名称总是让人担心。事实上,有些Java程序员特别不喜欢通配引入,以至于从不使用这个特性,而是让IDE帮他们生成一长串引入语句。通过将引入放置在需要这些引入的地方,你可以大幅减少可能的名称冲突。
9.5 重命名和隐藏方法
这样一来,JavaHashMap就是java.utiI.HashMap,而HashMap则对应
scala.collection.mutable.HashMap。
现在,HashMap很明确的便指向了scala.collection.mutable.HashMap,因为java.util.HashMap被隐藏起来了。
尖叫提示:
每个Scala程序都隐式地以如下代码开始:
import java.lang._
import scala._
import Predef._
和Java程序一样,java.lang总是被引入。接下来,scala包也被引入,不过方式有些特殊。不像所有其他引入,这个引入被允许可以覆盖之前的引入。举例来说,scala.StringBuilder覆盖java.lang.StringBuilder而不是与之冲突。最后,Predef对象被引入。它包含了相当多有用的函数,这些同样可以被放置在scala包对象中,不过Predef在Scala还没有加入包对象之前就存在了。
由于scala包默认被引入,对于那些以scala开头的包,你完全不需要写全这个前缀。例如:
collection.mutable.HashMap
上述代码和以下写法一样好:
scala.collection.mutable. HashMap
尖叫提示:
如果类声明为final,不能被继承。如果单个方法声明为final,将不能被重写。
10.2 重写方法
在scala中,若子类覆盖了父类的具体成员则必须带override修饰符;若是实现了同名的抽象成员时则override是可选的;若并未覆盖或实现基类中的成员则禁用override修饰符。重写一个非抽象方法需要用override修饰符,调用超类的方法使用super关键字。
toString方法返回一个字符串,并在构造完一个对象时被自动调用,返回结果交给解释器打印。该方法是所有Scala类隐式继承来的,如果不重写这个方法,就会用默认继承的版本。默认的toString方法来自于java.lang.Object类,其行为只是简单地打印类名、一个“@”符号和一个十六进制数。如果想让解释器输出更多有用的信息,则可以自定义toString方法。不过,这个方法是继承来的,要重写它必须在前面加上关键字“override”。
class Person {
var name = “”
override def toString = getClass.getName + “[name=” + name + “]”
}
class Employee extends Person {
var salary = 0.0
override def toString = super.toString + “[salary=” + salary + “]”
}
10.3 类型检查和转换
要测试某个对象是否属于某个给定的类,可以用isInstanceOf方法。用asInstanceOf方法将引用转换为子类的引用。classOf获取对象的类名。
10.10 Scala继承层级
1)在scala中,所有其他类都是AnyRef的子类,AnyVal和AnyRef都扩展自Any类。Any类是跟节点,Any中定义了isInstanceOf、asInstanceOf方法,以及哈希方法等。
2)Null类型的唯一实例就是null对象。可以将null赋值给任何引用,但不能赋值给值类型的变量。
3)Nothing类型没有实例。它对于泛型结构是有用处的,举例:空列表Nil的类型是List[Nothing],它是List[T]的子类型,T可以是任何类。
10.11无参方法与字段
Scala的无参方法在调用时,可以省略空括号。鉴于此,对用户代码而言,如果看不到类库的具体实现,那么调用无参方法和调用同名的字段则没有什么不同,甚至无法区分其具体实现到底是方法还是字段。如果把类库里的无参方法改成字段,或是把字段改成无参方法,那么客户代码不用更改也能运行。为了方便在这两种定义之间进行切换,Scala允许超类的无参方法被子类重写为字段,但是字段不能反过来被重写为无参方法,而且方法的返回类型必须和字段的类型一致。例如:
scala> class A {
| def justA() = “A”
| }
defined class A
scala> class B extends A {
| override val justA = “B”
| }
defined class B
scala> class C extends A {
| override val justA = 1
| }
:13: error: overriding method justA in class A of type ()String;
value justA has incompatible type
override val justA = 1
^
scala> class D {
| val d = 10
| }
defined class D
scala> class E extends D {
| override def d() = 100
| }
:13: error: overriding value d in class D of type Int;
method d needs to be a stable, immutable value
override def d() = 100
^
字段与方法的区别在于:字段一旦被初始化之后,就会被保存在内存中,以后每次调用都只需直接读取内存即可;方法不会占用内存空间,但是每次调用都需要执行一遍程序段,速度比字段要慢。因此,到底定义成无参方法还是字段,就是在速度和内存之间折衷。
字段能重写无参方法的原理是Scala只有两种命名空间:①值——字段、方法、包、单例对象;②类型——类、特质。因为字段和方法同处一个命名空间,所以字段可以重写无参方法。这也告诉我们,同处一个命名空间的定义类型,在同一个作用域内不能以相同的名字同时出现。例如,同一个类里不能同时出现同名的字段、无参方法和单例对象:
scala> class A {
| val a = 10
| object a
| }
:13: error: a is already defined as value a
object a
11. 特质
11.1 不允许多重集成
特质相当于接口,不能被实例化。特质定义使用trait关键字,与类相似,你同样可以在其中定义而不仅是声明字段和方法等。你可以使用extends或with将多个特质“混入”类中。注意当在定义特质时,使用extends指定了特质的超类,那么该特质就只能混入扩展了指定的超类的类中。单例对象天生就是具体的,特质天生就是抽象的,不过不需要用“abstract”来说明。所以,特质可以包含抽象成员,而单例对象却不行。
特质对混入有一个限制条件:那就是要混入该特质的类/单例对象/特质,它的超类必须是待混入特质的超类,或者是待混入特质的超类的子类。因为特质是多重继承的替代品,那就有“继承”的意思。既然是继承,混入特质的类/单例对象/特质的层次,就必须比待混入特质的层次要低。
特质与类的区别在于:
1)特质不能带有“类参数”,也即传递给主构造器的参数;
2)不论在类的哪个地方,super调用都是静态绑定的,但在特质中,它们是动态绑定的,因为在特质定义时,尚且不知道它的超类是谁,因为它还没有“混入”,由于在特质中使用super调用超类方法是动态绑定的,因此你需要对特质中相应的方法加上abstract声明(虽然加上了abstract声明,但方法仍可以被具体定义,这种用法只有在特质中有效),以告诉编译器特质中的该方法只有在特质被混入某个具有期待方法的具体定义的类中才有效。你需要非常注意特质被混入的次序:特质在构造时顺序是从左到右,构造器的顺序是类的线性化(线性化是描述某个类型的所有超类型的一种技术规格)的反向。由于多态性,子类的方法最先起作用,因此越靠近右侧的特质越先起作用,如果最右侧特质调用了super,它调用左侧的特质的方法,依此类推。
Ordering特质:
Ordering特质扩展自java的Comparator接口。Ordering特质也用于排序,为了使用它,你需要做的是:定义一个该特质的子类的单独的实例,需要实现其中的compare方法,并将其作为参数传递给排序函数。此乃策略模式也。
Application特质:
特质Application声明了带有合适签名的main方法。但是它存在一些问题,所以只有当程序相对简单并且是单线程的情况下才可以继承Application特质。Application特质相对于APP特质来说,有些陈旧,你应该使用更新的APP特质。
APP特质:
APP特质同Application特质一样,都提供了带有合适签名的main方法,在使用时只需将它混入你的类中,然后就可以在类的主构造器中写代码了,无需再定义main方法。如果你需要命令行参数,可以通过args属性得到。
11.2 当做接口使用的特质
特质中没有实现的方法就是抽象方法。类通过extends继承特质,通过with可以继承多个特质。
trait Logger {
def log(msg: String)
}
class ConsoleLogger extends Logger with Cloneable with Serializable {
def log(msg: String) {
println(msg)
}
}
Logger with Cloneable with Serializable是一个整体,extends这个整体,所有的java接口都可以当做Scala特质使用。
11.3 带有具体实现的特质
特质中的方法并不一定是抽象的:
trait ConsoleLogger {
def log(msg: String) {
println(msg)
}
}
class Account {
protected var balance = 0.0
}
class SavingsAccount extends Account with ConsoleLogger {
def withdraw(amount: Double) {
if (amount > balance) log(“余额不足”)
else balance -= amount
}
}
11.4 带有特质的对象
在构建对象时混入某个具体的特质,覆盖掉抽象方法,提供具体实现:
trait Logger {
def log(msg: String)
}
trait ConsoleLogger extends Logger {
def log(msg: String) {
println(msg)
}
}
class Account {
protected var balance = 0.0
}
abstract class SavingsAccount extends Account with Logger {
def withdraw(amount: Double) {
if (amount > balance) log(“余额不足”)
else balance -= amount
}
}
object Main extends App {
val account = new SavingsAccount with ConsoleLogger
account.withdraw(100)
}
11.5 叠加在一起的特质
super并不是指继承关系,而是指的加载顺序。继承多个相同父特质的类,会从右到左依次调用特质的方法。Super指的是继承特质左边的特质,从源码是无法判断super.method会执行哪里的方法,如果想要调用具体特质的方法,可以指定:super[ConsoleLogger].log(…).其中的泛型必须是该特质的直接超类类型
trait Logger {
def log(msg: String);
}
trait ConsoleLogger extends Logger {
def log(msg: String) {
println(msg)
}
}
trait TimestampLogger extends ConsoleLogger {
override def log(msg: String) {
super.log(new java.util.Date() + " " + msg)
}
}
trait ShortLogger extends ConsoleLogger {
override def log(msg: String) {
super.log(if (msg.length <= 15) msg else s"KaTeX parse error: Expected 'EOF', got '}' at position 31: …0, 12)}...") }̲ } class Accoun…{msg.substring(0, 12)}…")
}
}
trait ConsoleLogger2 extends Logger2 {
override def log(msg: String) {
println(msg)
}
}
class Account2 {
protected var balance = 0.0
}
abstract class SavingsAccount2 extends Account2 with Logger2 {
def withdraw(amount: Double) {
if (amount > balance) log(“余额不足”)
else balance -= amount
}
}
object Main2 extends App {
val acct1 = new SavingsAccount2 with ConsoleLogger2 with TimestampLogger2 with ShortLogger2
acct1.withdraw(100)
}
11.7 当做富接口使用的特质
即该特质中既有抽象方法,又有非抽象方法
//富特质
trait Logger3 {
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)
}
}
trait ConsoleLogger3 extends Logger3 {
def log(msg: String) {
println(msg)
}
}
class Account3 {
protected var balance = 0.0
}
abstract class SavingsAccount3 extends Account3 with Logger3 {
def withdraw(amount: Double) {
if (amount > balance) severe(“余额不足”)
else balance -= amount
}
}
object Main3 extends App {
val acct = new SavingsAccount with ConsoleLogger
acct.withdraw(100)
}
11.8 特质中的具体字段
特质中可以定义具体字段,如果初始化了就是具体字段,如果不初始化就是抽象字段。
混入该特质的类就具有了该字段,字段不是继承,而是简单的加入类。是自己的字段。
trait Logger4 {
def log(msg: String)
}
trait ConsoleLogger4 extends Logger4 {
def log(msg: String) {
println(msg)
}
}
trait ShortLogger4 extends Logger4 {
val maxLength = 15
abstract override def log(msg: String) {
super.log(if (msg.length <= maxLength) msg else s”KaTeX parse error: Expected 'EOF', got '}' at position 42: …h - 3)}...") }̲ } class Accoun…{msg.substring(0, maxLength - 3)}…”)
}
}
class Account5 {
protected var balance = 0.0
}
abstract class SavingsAccount5 extends Account5 with Logger5 {
var interest = 0.0
def withdraw(amount: Double) {
if (amount > balance) log(“余额不足”)
else balance -= amount
}
}
object Main5 extends App {
val acct = new SavingsAccount5 with ConsoleLogger5 with ShortLogger5 {
val maxLength = 20
}
acct.withdraw(100)
println(acct.maxLength)
}
11.10 特质构造顺序
特质也是有构造器的,构造器中的内容由“字段的初始化”和一些其他语句构成
trait Logger6 {
println(“我在Logger6特质构造器中,嘿嘿嘿。。。”)
def log(msg: String)
}
trait ConsoleLogger6 extends Logger6 {
println(“我在ConsoleLogger6特质构造器中,嘿嘿嘿。。。”)
def log(msg: String) {
println(msg)
}
}
trait ShortLogger6 extends Logger6 {
val maxLength: Int
println(“我在ShortLogger6特质构造器中,嘿嘿嘿。。。”)
abstract override def log(msg: String) {
super.log(if (msg.length <= maxLength) msg else s"${msg.substring(0, maxLength - 3)}…")
}
}
class Account6 {
println(“我在Account6构造器中,嘿嘿嘿。。。”)
protected var balance = 0.0
}
abstract class SavingsAccount6 extends Account6 with ConsoleLogger6 with ShortLogger6{
println(“我再SavingsAccount6构造器中”)
var interest = 0.0
override val maxLength: Int = 20
def withdraw(amount: Double) {
if (amount > balance) log(“余额不足”)
else balance -= amount
}
}
object Main6 extends App {
val acct = new SavingsAccount6 with ConsoleLogger6 with ShortLogger6
acct.withdraw(100)
println(acct.maxLength)
}
步骤总结:
1、调用当前类的超类构造器
2、第一个特质的父特质构造器
3、第一个特质构造器
4、第二个特质构造器的父特质构造器由于已经执行完成,所以不再执行
5、第二个特质构造器
6、当前类构造器
11.11 初始化特质中的字段
特质不能有构造器参数,每个特质都有一个无参数的构造器。缺少构造器参数是特质与类之间唯一的技术差别。除此之外,特质可以具备类的所有特性,比如具体的和抽象的字段,以及超类。现在有如下情景:我们想通过特质来实现日志数据的输出,输出到某一个文件中
import java.io.PrintStream
trait Logger7{
def log(msg:String)
}
trait FileLogger7 extends Logger7{
val fileName:String
val out = new PrintStream(fileName)
override def log(msg: String): Unit = {
out.print(msg)
out.flush()
}
}
class SavingsAccount7{
}
object Main7 extends App {
val acct = new SavingsAccount7 with FileLogger7 {
override val fileName = “2017-11-24.log”//空指针异常
}
}
如果想修复如上错误,可以:
可以为类,方法,字段局部变量,参数,表达式,类型参数以及各种类型定义添加注解
@Entity class Student
@Test def play() {}
@BeanProperty var username = _
def doSomething(@NotNull message: String) {}
@BeanProperty @Id var username = _
构造器注解,需要在主构造器之前,类名之后,且需要加括号,如果注解有参数,则写在注解括号里
class Student @Inject() (var username: String, var password: String)
为表达式添加注解,在表达式后添加冒号
(map1.get(key): @unchecked) match {…}
泛型添加注解
class Student[@specialized T]
实际类型添加注解
String @cps[Unit]
12.2 注解参数
Java注解可以有带名参数:
@Test(timeout = 100, expected = classOf[IOException])
// 如果参数名为value,则该名称可以直接略去。
@Named(“creds”) var credentials: Credentials = _ // value参数的值为 “creds”
// 注解不带参数,圆括号可以省去
@Entity class Credentials
Java 注解的参数类型只能是:
数值型的字面量
字符串
类字面量
Java枚举
其他注解
上述类型的数组(但不能是数组的数组),Scala注解可以是任何类型,但只有少数几个Scala注解利用了这个增加的灵活性。
12.3 注解实现
你可以实现自己的注解,但是更多的是使用Scala和Java提供的注解。注解必须扩展Annotation特质:
class unchecked extends annotation.Annotation
12.4 针对Java的注解
Java修饰符:对于那些不是很常用的Java特性,Scala使用注解,而不是修饰符关键字。
@volatile var done = false // JVM中将成为volatile的字段
@transient var recentLookups = new HashMap[String, String] // 在JVM中将成为transient字段,该字段不会被序列化。
@strictfp def calculate(x: Double) = …
@native def win32RegKeys(root: Int, path: String): Array[String]
标记接口:Scala用注解@cloneable和@remote 而不是 Cloneable和Java.rmi.Remote“标记接口”来标记可被克隆的对象和远程的对象。
@cloneable class Employee
受检异常:和Scala不同,Java编译器会跟踪受检异常。如果你从Java代码中调用Scala的方法,其签名应包含那些可能被抛出的受检异常。用@throws注解来生成正确的签名。
class Book {
@throws (classOf[IOException]) def read(filename: String) { … }
…
}
Java版本的方法签名:
void read(String fileName) throws IOException
// 如果没有@throws注解,Java代码将不能捕获该异常
try {//Java代码
book.read(“war-and-peace.txt”);
} catch (IOException ex) {
…
}
即:Java编译期需要在编译时就知道read方法可以抛IOException异常,否则Java会拒绝捕获该异常。
12.5 由于优化的注解
尾递归的优化
啥玩是尾递归?
尾递归:
def story(): Unit = {从前有座山,山上有座庙,庙里有个老和尚,一天老和尚对小和尚讲故事:story()}
尖叫提示:
进入下一个函数不再需要上一个函数的环境了,得出结果以后直接返回。
非尾递归:
def story(): Unit = {从前有座山,山上有座庙,庙里有个老和尚,一天老和尚对小和尚讲故事:story(),小和尚听了,找了块豆腐撞死了}
尖叫提示:
下一个函数结束以后此函数还有后续,所以必须保存本身的环境以供处理返回值。
递归调用有时候能被转化成循环,这样能节约栈空间:
object Util {
def sum(xs: Seq[Int]): BigInt = {
if (xs.isEmpty) 0 else xs.head + sum(xs.tail)
}
…
}
上面的sum方法无法被优化,因为计算过程中最后一步是加法,而不是递归调用。调整后的代码:
def sum2(xs: Seq[Int], partial: BigInt): BigInt = {
if (xs.isEmpty) partial else sum2(xs.tail, xs.head + partial)
}
Scala编译器会自动对sum2应用“尾递归”优化。如果你调用sum(1 to 1000000) 将会发生一个栈溢出错误。不过sum2(1 to 1000000, 0) 将会得到正确的结果。
尽管Scala编译器会尝试使用尾递归优化,但有时候某些不太明显的原因会造成它无法这样做。如果你想编译器无法进行优化时报错,则应该给你的方法加上@tailrec注解。
尖叫提示:
对于消除递归,一个更加通用的机制叫做“蹦床”。蹦床的实现会将执行一个循环,不停的调用函数。每个函数都返回下一个将被调用的函数。尾递归在这里是一个特例,每个函数都返回它自己。*Scala有一个名为TailCalls的工具对象,帮助我们轻松实现蹦床:
import scala.util.control.TailCalls._
def evenLength(xs: Seq[Int]): TailRec[Boolean] = {
if(xs.isEmpty) done(true) else tailcall(oddLength(xs.tail))
}
def oddLength(xs: Seq[Int]): TailRec[Boolean] = {
if(xs.isEmpty) done(false) else tailcall(evenLength(xs.tail))
}
// 获得TailRec对象获取最终结果,可以用result方法
evenLength(1 to 1000000).result
13. 类型参数
13.1 泛类型
类和特质都可以带类型参数,用方括号来定义类型参数,可以用类型参数来定义变量、方法参数和返回值。带有一个或多个类型参数的类是泛型的。如下p1,如果实例化时没有指定泛型类型,则scala会自动根据构造参数的类型自动推断泛型的具体类型。
class Pair[T, S](val first: T, val second: S) {
override def toString = “(” + first + “,” + second + “)”
}
//从构造参数推断类型
val p1 = new Pair(42, “String”)
//设置类型
val p2 = new PairAny, Any
13.2 泛型函数
函数或方法也可以有类型(泛型)参数。
// 从参数类型来推断类型
println(getMiddle(Array(“Bob”, “had”, “a”, “little”, “brother”)).getClass.getTypeName)
//指定类型,并保存为具体的函数。
val f = getMiddle[String] _
println(f(Array(“Bob”, “had”, “a”, “little”, “brother”)))
13.3 类型变量限定
在Java泛型里不表示某个泛型是另外一个泛型的子类型可以使用extends关键字,而在scala中使用符号“<:”,这种形式称之为泛型的上界。
class Pair1[T <: Comparable[T]](val first: T, val second: T) {
def smaller = if (first.compareTo(second) < 0) first else second
}
object Main1 extends App{
override def main(args: Array[String]): Unit = {
val p = new Pair1(“Fred”, “Brooks”)
println(p.smaller)
}
}
在Java泛型里表示某个泛型是另外一个泛型的父类型,使用super关键字,而在scala中,使用符号“>:”,这种形式称之为泛型的下界。
class Pair2[T](val first: T, val second: T) {
def replaceFirst[R >: T](newFirst: R) = new Pair2[R](newFirst, second)
override def toString = “(” + first + “,” + second + “)”
}
object Main2 extends App{
override def main(args: Array[String]): Unit = {
val p = new Pair2(“Nick”, “Alice”)
println§
println(p.replaceFirst(“Joke”))
println§
}
}
在Java中,T同时是A和B的子类型,称之为多界,形式如:
13.5 视图界定
在Scala中,如果你想标记某一个泛型可以隐式的转换为另一个泛型,可以使用:[T <% Comparable[T]],由于Scala的Int类型没有实现Comparable接口,所以我们需要将Int类型隐式的转换为RichInt类型,比如:
class Pair3[T <% Comparable[T]](val first: T, val second: T) {
def smaller = if (first.compareTo(second) < 0) first else second
override def toString = “(” + first + “,” + second + “)”
}
object Main3 extends App {
val p = new Pair3(4, 2)
println(p.smaller)
}
13.6 上下文界定
视图界定 T <% V要求必须存在一个从T到V的隐式转换。上下文界定的形式为T:M,其中M是另一个泛型类,它要求必须存在一个类型为M[T]的隐式值。
下面类定义要求必须存在一个类型为Ordering[T]的隐式值,当你使用了一个使用了隐式值得方法时,传入该隐式参数。
class Pair4[T: Ordering](val first: T, val second: T) {
def smaller(implicit ord: Ordering[T]) = {
println(ord)
if (ord.compare(first, second) < 0) first else second
}
override def toString = “(” + first + “,” + second + “)”
}
object Main4 extends App{
override def main(args: Array[String]): Unit = {
val p4 = new Pair4(1, 2)
println(p4.smaller)
}
}
13.7 Manifest上下文界定
Manifest是scala2.8引入的一个特质,用于编译器在运行时也能获取泛型类型的信息。在JVM上,泛型参数类型T在运行时是被“擦拭”掉的,编译器把T当作Object来对待,所以T的具体信息是无法得到的;为了使得在运行时得到T的信息,scala需要额外通过Manifest来存储T的信息,并作为参数用在方法的运行时上下文。
def test[T] (x:T, m:Manifest[T]) { … }
有了Manifest[T]这个记录T类型信息的参数m,在运行时就可以根据m来更准确的判断T了。但如果每个方法都这么写,让方法的调用者要额外传入m参数,非常不友好,且对方法的设计是一道伤疤。好在scala中有隐式转换、隐式参数的功能,在这个地方可以用隐式参数来减轻调用者的麻烦。
def foo[T](x: List[T])(implicit m: Manifest[T]) = {
println(m)
if (m <:< manifest[String])
println(“Hey, this list is full of strings”)
else
println(“Non-stringy list”)
}
foo(List(“one”, “two”))
foo(List(1, 2))
foo(List(“one”, 2))
隐式参数m是由编译器根据上下文自动传入的,比如上面是编译器根据 “one”,”two” 推断出 T 的类型是 String,从而隐式的传入了一个Manifest[String]类型的对象参数,使得运行时可以根据这个参数做更多的事情。
不过上面的foo 方法定义使用隐式参数的方式,仍显得啰嗦,于是scala里又引入了“上下文绑定”,
def foo[T](x: List[T]) (implicit m: Manifest[T])
可以简化为:
def foo[T:Manifest] (x: List[T])
在引入Manifest的时候,还引入了一个更弱一点的ClassManifest,所谓的弱是指类型信息不如Manifest那么完整,主要针对高阶类型的情况
scala在2.10里却用TypeTag替代了Manifest,用ClassTag替代了ClassManifest,原因是在路径依赖类型中,Manifest存在问题:
scala> class Foo{class Bar}
defined class Foo
scala> def m(f: Foo)(b: f.Bar)(implicit ev: Manifest[f.Bar]) = ev
warning: there were 2 deprecation warnings; re-run with -deprecation for details
m: (f: Foo)(b: f.Bar)(implicit ev: Manifest[f.Bar])Manifest[f.Bar]
scala> val f1 = new Foo;val b1 = new f1.Bar
f1: Foo = Foo@681e731c
b1: f1.Bar = Foo B a r @ 271768 a b s c a l a > v a l f 2 = n e w F o o ; v a l b 2 = n e w f 2. B a r f 2 : F o o = F o o @ 3 e 50039 c b 2 : f 2. B a r = F o o Bar@271768ab scala> val f2 = new Foo;val b2 = new f2.Bar f2: Foo = Foo@3e50039c b2: f2.Bar = Foo Bar@271768abscala>valf2=newFoo;valb2=newf2.Barf2:Foo=Foo@3e50039cb2:f2.Bar=FooBar@771d16b9
scala> val ev1 = m(f1)(b1)
warning: there were 2 deprecation warnings; re-run with -deprecation for details
ev1: Manifest[f1.Bar] = [email protected]#FooKaTeX parse error: Expected 'EOF', got '#' at position 154: …[email protected]#̲FooBar
scala> ev1 == ev2 // they should be different, thus the result is wrong
res28: Boolean = true
了解之后,我们总结一下,TypeTag到底有啥用呢?看下面的例子:
请留意:
=:=,意思为:type equality
<:< ,意思为:subtype relation
类型判断不要用 == 或 !=
class Animal{}
class Dog extends Animal{}
object MainFoo extends App{
override def main(args: Array[String]): Unit = {
val list1 = List(1, 2, 3)
val list2 = List(“1”, “2”, “3”)
val list3 = List(“1”, “2”, 3)
def test1(x: List[Any]) = {
x match {
case list: List[Int] => “Int list”
case list: List[String] => “String list”
case list: List[Any] => “Any list”
}
}
println(test1(list1))
println(test1(list2))
println(test1(list3))
import scala.reflect.runtime.universe._
def test2[A : TypeTag](x: List[A]) = typeOf[A] match {
case t if t =:= typeOf[String] => “String List”
case t if t <:< typeOf[Animal] => “Dog List”
case t if t =:= typeOf[Int] => “Int List”
}
println(test2(List(“string”)))
println(test2(List(new Dog)))
println(test2(List(1, 2)))
}
}
13.8 多重界定
不能同时有多个上界或下界,变通的方式是使用复合类型
T <: A with B
T >: A with B
可以同时有上界和下界,如
T >: A <: B
这种情况下界必须写在前边,上界写在后边,位置不能反。同时A要符合B的子类型,A与B不能是两个无关的类型。
可以同时有多个view bounds
T <% A <% B
这种情况要求必须同时存在 T=>A的隐式转换,和T=>B的隐式转换。
class A{}
class B{}
implicit def string2A(s:String) = new A
implicit def string2B(s:String) = new B
def foo2 T <% A <% B = println(“foo2 OK”)
foo2(“test”)
可以同时有多个上下文界定
T : A : B
这种情况要求必须同时存在C[T]类型的隐式值,和D[T]类型的隐式值。
class C[T];
class D[T];
implicit val c = new C[Int]
implicit val d = new D[Int]
def foo3 T : C : D = println(“foo3 OK”)
foo3(2)
13.9 类型约束
类型约束,提供了限定类型的另一种方式,一共有3中关系声明:
T =:= U意思为:T类型是否等于U类型
T <:< U意思为:T类型是否为U或U的子类型
T <%
class Pair5[T] (val first: T, val second: T)(implicit ev: T <:< Comparable[T]){}
使用举例:
import java.io.File
class Pair6[T](val first: T, val second: T) {
def smaller(implicit ev: T <:< Ordered[T]) = {
if(first < second) first else second
}
}
object Main6 extends App{
override def main(args: Array[String]): Unit = {
//构造Pair6[File]时,注意此时是不会报错的
val p6 = new Pair6[File](new File(""), new File(""))
//这就报错了
p6.smaller
}
}
13.10 型变
术语:
英文 中文 示例
Variance 型变 Function[-T, +R]
Nonvariant 不变 Array[A]
Covariant 协变 Supplier[+A]
Contravariant 逆变 Consumer[-A]
Immutable 不可变 String
Mutable 可变 StringBuilder
其中,Mutable常常意味着Nonvariant,但是Noncovariant与Mutable分别表示两个不同的范畴。
即:可变的,一般意味着“不可型变”,但是“不可协变”和可变的,分别表示两个不同范畴。
型变(Variance)拥有三种基本形态:协变(Covariant), 逆变(Contravariant), 不变(Nonconviant),可以形式化地描述为:
一般地,假设类型C[T]持有类型参数T;给定两个类型A和B,如果满足A <: B,则C[A]与 C[B]之间存在三种关系:
如果C[A] <: C[B],那么C是协变的(Covariant);
如果C[A] :> C[B],那么C是逆变的(Contravariant);
否则,C是不变的(Nonvariant)。
Scala的类型参数使用+标识“协变”,-标识“逆变”,而不带任何标识的表示“不变”(Nonvariable):
trait C[+A] // C is covariant
trait C[-A] // C is contravariant
trait C[A] // C is nonvariant
如何判断一个类型是否有型变能力:
一般地,“不可变的”(Immutable)类型意味着“型变”(Variant),而“可变的”(Mutable)意味着“不变”(Nonvariant)。
其中,对于不可变的(Immutable)类型C[T]
如果它是一个生产者,其类型参数应该是协变的,即C[+T];
如果它是一个消费者,其类型参数应该是逆变的,即C[-T]。
14. 隐式转换和隐式参数
14.1 显式转换与隐式转换
1)显式类型转换
正如之前所述的,scala中类型转换使用方法实现,以下是显式类型测试和显式类型转换的示例:
a.isInstanceOf[String] // 显式类型测试
a.asInstanceOf[String] // 显式类型转换
2)隐式类型转换
隐式转换只是普通的方法,唯一特殊的地方是它以修饰符implicit开始,implicit告诉scala编译器可以在一些情况下自动调用(比如说如果当前类型对象不支持当前操作,那么scala编译器就会自动添加调用相应隐式转换函数的代码,将其转换为支持当前操作的类型的对象,前提是已经存在相应的隐式转换函数且满足作用域规则),而无需你去调用(当然如果你愿意,你也可以自行调用)。隐式转换函数定义如下:
implicit def functionName(…) = {…}
隐式转换满足以下规则:
作用域规则:scala编译器仅会考虑处于作用域之内的隐式转换。隐式转换要么是以单一标识符的形式(即不能是aaa.bbb的形式,应该是bbb的形式)出现在作用域中,要么是存在于源类型或者目标类型的伴生对象中。
单一调用规则:编译器在同一个地方只会添加一次隐式操作,不会在添加了一个隐式操作之后再在其基础上添加第二个隐式操作。
显式操作先行规则:若编写的代码类型检查无误,则不会尝试任何隐式操作。
举例:
implicit def a(d: Double) = d.toInt
//不加上边这句你试试
val i1: Int = 3.5
println(i1)
14.2 利用隐式转换丰富类库功能
如果需要为一个类增加一个方法,可以通过隐式转换来实现。比如想为File增加一个read方法,可以如下定义:
class RichFile(val from: File) {
def read = Source.fromFile(from.getPath).mkString
}
implicit def file2RichFile(from: File) = new RichFile(from)
val contents = new File(“C:\Users\61661\Desktop\scala笔记.txt”).read
println(contents)
有什么好处呢?好处就是你可以不修改原版本的代码而为原本的代码增加新功能。
14.3 隐式值
将name变量标记为implicit,所以编译器会在方法省略隐式参数的情况下去搜索作用域内的隐式值作为缺少参数。
implicit val name = “Nick”
def person(implicit name: String) = name
println(person)
但是如果此时你又相同作用域中定义一个隐式变量,再次调用方法时就会报错:
implicit val name = “Nick”
implicit val name2 = “Nick”
def person(implicit name: String) = name
println(person)
14.4 隐式视图
隐式转换为目标类型:把一种类型自动转换到另一种类型
def foo(msg : String) = println(msg)
implicit def intToString(x : Int) = x.toString
foo(10)
隐式转换调用类中本不存在的方法
class Dog {
val name = “金毛”
}
class Skill{
def fly(animal: Dog, skill: String) = println(animal.name + “已领悟” + skill)
}
object Learn{
implicit def learningType(s : Dog) = new Skill
}
object Main2 extends App{
override def main(args: Array[String]): Unit = {
import unit15.Learn._
val dog = new Dog
dog.fly(dog, “飞行技能”)
}
}
当然了,以上操作也可以定义在包对象中,即,在object Learn的外面再套一层,package,没问题的!
14.5 隐式类
在scala2.10后提供了隐式类,可以使用implicit声明类,但是需要注意以下几点:
—– 其所带的构造参数有且只能有一个
—– 隐式类必须被定义在“类”或“伴生对象”或“包对象”里
—– 隐式类不能是case class(case class在定义会自动生成伴生对象与2矛盾)
—– 作用域内不能有与之相同名称的标示符
object StringUtils {
implicit class StringImprovement(val s : String){ //隐式类
def increment = s.map(x => (x +1).toChar)
}
}
object Main3 extends App{
import unit15.StringUtils._
println(“mobin”.increment)
}
14.6 隐式的转换时机
文件内容转数组:
val array= file1.getLines.toArray
文件内容转字符串:
val iterator = file1.mkString
15.2 读取字符
由于Source.fromFile直接返回的就是Iterator[Char],所以可以直接对其进行迭代,按照字符访问里边每一个元素。
Source.fromFile(“C:\Users\61661\Desktop\scala笔记.txt”, “UTF-8”)
for(ch <- file2){
println(ch)
}
file2.close
15.3 读取词法单元和数字
如果想将以某个字符或某个正则表达式分开的字符成组读取,可以这么做:
val file3 = Source.fromFile(“D:\BigData课堂笔记\尚硅谷BigData笔记\尚硅谷大数据技术之Scala\2.资料\info.csv”)
val tokens = file3.mkString.split(",")
println(tokens.mkString(" "))
file3.close
15.4 读取网络资源、文件写入、控制台操作
读取网络资源
val webFile = Source.fromURL(“http://www.baidu.com”)
webFile.foreach(print)
webFile.close()
写入数据到文件
import java.io.{File, PrintWriter}
val writer = new PrintWriter(new File(“嘿嘿嘿.txt”))
for (i <- 1 to 100)
writer.println(i)
writer.close()
控制台操作
//控制台交互–老API
print(“请输入内容:”)
val consoleLine1 = Console.readLine()
println(“刚才输入的内容是:” + consoleLine1)
//控制台交互–新API
print(“请输入内容(新API):”)
val consoleLine2 = StdIn.readLine()
println(“刚才输入的内容是:” + consoleLine2)
15.5 序列化
@SerialVersionUID(1L) class Person extends Serializable{
override def toString = name + “,” + age
val name = “Nick”
val age = 20
}
object PersonMain extends App{
override def main(args: Array[String]): Unit = {
import java.io.{FileOutputStream, FileInputStream, ObjectOutputStream, ObjectInputStream}
val nick = new Person
val out = new ObjectOutputStream(new FileOutputStream(“Nick.obj”))
out.writeObject(nick)
out.close()
val in = new ObjectInputStream(new FileInputStream(“Nick.obj”))
val saveNick = in.readObject()
in.close()
println(saveNick)
}
}
15.6 进程控制
我们可以使用scala来操作shell,scala提供了scala.sys.process包提供了用于shell程序交互的工具。
执行shell
import sys.process._
“ls -al /”!
“ls -al /”!!
尖叫提示:!和!!的区别在于:process包中有一个将字符串隐式转换成ProcessBuild对象的功能,感叹号就是执行这个对象,单感叹号的意思就是程序执行成功返回0,执行失败返回非0,如果双感叹号,则结果以字符串的形式返回。
管道符
import sys.process._
“ls -al /” #| “grep etc” !
将shell的执行结果重定向到文件
import sys.process._
“ls -al /” #| “grep etc” !;
“ls -al /” #>> new File(“output.txt”) !;
尖叫提示:
注意,每一个感叹号后边,有分号结束
scala进程还可以提供:
p #&& q操作,即p任务执行成功后,则执行q任务。
p #|| q操作,即p任务执行不成功,则执行q任务。
既然这么强大,那么crontab + scala + shell,就完全不需要使用oozie了。
15.7 正则表达式
我们可以通过正则表达式匹配一个句子中所有符合匹配的内容,并输出:
import scala.util.matching.Regex
val pattern1 = new Regex("(S|s)cala")
val pattern2 = “(S|s)cala”.r
val str = “Scala is scalable and cool”
println((pattern2 findAllIn str).mkString(","))
16. 高级类型
16.1 类型与类的区别
在Java里,一直到jdk1.5之前,我们说一个对象的类型(type),都与它的class是一一映射的,通过获取它们的class对象,比如 String.class, int.class, obj.getClass() 等,就可以判断它们的类型(type)是不是一致的。 而到了jdk1.5之后,因为引入了泛型的概念,类型系统变得复杂了,并且因为jvm选择了在运行时采用类型擦拭的做法(兼容性考虑),类型已经不能单纯的用class来区分了,比如 List 和 List 的class 都是 Class,然而两者类型(type)却是不同的。泛型类型的信息要通过反射的技巧来获取,同时java里增加了Type接口来表达更泛的类型,这样对于 List这样由类型构造器和类型参数组成的类型,可以通过 Type 来描述;它和 List 类型的对应的Type对象是完全不同的。
在Scala里,类型系统又比java复杂很多,泛型从一开始就存在,还支持高阶的概念(后续会讲述)。所以它没有直接用Java里的Type接口,而是自己提供了一个scala.reflect.runtime.universe.Type(2.10后)
在scala里获取类型信息是比较便捷的:
class A{}
object TypeSyllabus {
def main(args: Array[String]): Unit = {
import scala.reflect.runtime.universe._
println(typeOf[A])
}
}
同样scala里获取类(Class)信息也很便捷,类似:
class A{}
object TypeSyllabus {
def main(args: Array[String]): Unit = {
import scala.reflect.runtime.universe._
println(typeOf[A])
println(classOf[A])
}
}
尖叫提示:注意,typeOf 和 classOf 方法接收的都是类型符号(symbol),并不是对象实例。
16.2 classOf与getClass的区别
获取Class时的两个方法:classOf 和 getClass
scala> class A
scala> val a = new A
scala> a.getClass
res2: Class[_ <: A] = class A
scala> classOf[A]
res3: Class[A] = class A
上面显示了两者的不同,getClass方法得到的是 Class[A]的某个子类,而 classOf[A] 得到是正确的 Class[A],但是去比较的话,这两个类型是equals为true的。
这种细微的差别,体现在类型赋值时,因为java里的 Class[T]是不支持协变的,所以无法把一个 Class[_ < : A] 赋值给一个 Class[A]。
16.3 单例类型
16.4 类型投影
在scala里,内部类型(排除定义在object内部的),想要表达所有的外部类A实例路径下的B类型,即对 a1.B 和 a2.B及所有的 an.B类型找一个共同的父类型,这就是类型投影,用 A#B的形式表示。
A#B
/ \
/ \
a1.B a2.B
我们回头来对比一下scala里的类型投影与java里的内部类型的概念,java里的内部类型在写法上是 Outter.Inner 它其实等同于scala里的投影类型 Outter#Inner,java里没有路径依赖类型的概念,比较简化。
16.5 类型别名
可以通过type关键字来创建一个简单的别名,类型别名必须被嵌套在类或者对象中,不能出现在scala文件的顶层:
class Document {
import scala.collection.mutable._
type Index = HashMap[String, (Int, Int)]
}
def play(x: Index): Unit ={}
16.6 结构类型
结构类型是指一组关于抽象方法、字段和类型的规格说明,你可以对任何具备append方法的类的实例调用appendLines方法,这种方式比定义特质更加灵活,是通过反射进行调用的:
class Structure {
def play() = println(“play方法调用了”)
}
object HelloStructure {
def main(args: Array[String]): Unit = {
type X = {def play(): Unit} //type关键字是把 = 后面的内容命名为别名。
def init(res: X) = res.play //本地方法
init(new {
def play() = println(“Played”)
})
init(new {
def play() = println(“Play再一次”)
})
object A {
def play() {
println(“A object play”)
}
}
init(A)
val structure = new Structure
init(structure)
}
}
总结:
结构类型,简单来说,就是只要是传入的类型,符合之前定义的结构的,都可以调用。
16.7 复合类型
class A extends B with C with D with E
应做类似如下形式解读:
class A extends (B with C with D with E)
T1 with T2 with T3 …
这种形式的类型称为复合类型(compound type)或者也叫交集类型(intersection type)。
也可以通过type的方式声明符合类型:
type X = X1 with X2
16.8 中置类型
中置类型是一个带有两个类型参数的类型,以中置语法表示,比如可以将Map[String, Int]表示为:
val scores: String Map Int = Map(“Fred” -> 42)
16.9 自身类型
self => 这句相当于给this起了一个别名为self:
class A {
self => //this别名
val x=2
def foo = self.x + this.x
}
self不是关键字,可以用除了this外的任何名字命名(除关键字)。就上面的代码,在A内部,可以用this指代当前对象,也可以用self指代,两者是等价的。
它的一个场景是用在有内部类的情况下:
class Outer { outer =>
val v1 = “here”
class Inner {
println(outer.v1) // 用outer表示外部类,相当于Outer.this
}
}
对于this别名 self =>这种写法形式,是自身类型(self type)的一种特殊方式。self在不声明类型的情况下,只是this的别名,所以不允许用this做this的别名。
16.10 运行时反射
scala编译器会将scala代码编译成JVM字节码,编译过程中会擦除scala特有的一些类型信息,在scala-2.10以前,只能在scala中利用java的反射机制,但是通过java反射机制得到的是只是擦除后的类型信息,并不包括scala的一些特定类型信息。从scala-2.10起,scala实现了自己的反射机制,我们可以通过scala的反射机制得到scala的类型信息。
给定类型或者对象实例,通过scala运行时反射,可以做到:1)获取运行时类型信息;2)通过类型信息实例化新对象;3)访问或调用对象的方法和属性等。
16.10.1 获取运行时类型信息
scala运行时类型信息是保存在TypeTag对象中,编译器在编译过程中将类型信息保存到TypeTag中,并将其携带到运行期。我们可以通过typeTag方法获取TypeTag类型信息。
import scala.reflect.runtime.universe._
val typeTagList = typeTag[List[Int]]//得到了包装Type对象的TypeTag对象
println(typeTagList)
或者使用:
typeOf[List[Int]]//直接得到了Type对象
尖叫提示:Type对象是没有被类型擦除的
我们可以通过typeTag得到里面的type,再通过type得到里面封装的各种内容:
import scala.reflect.runtime.universe._
val typeTagList = typeTag[List[Int]]
println(typeTagList)
println(typeTagList.tpe)
println(typeTagList.tpe.decls.take(10))
16.10.2 运行时类型实例化
我们已经知道通过Type对象可以获取未擦除的详尽的类型信息,下面我们通过Type对象中的信息找到构造方法并实例化类型的一个对象:
class Person(name:String, age: Int) {
def myPrint() = {
println(name + “,” + age)
}
}
object PersonMain extends App{
override def main(args: Array[String]): Unit = {
//得到JavaUniverse用于反射
val ru = scala.reflect.runtime.universe
//得到一个JavaMirror,一会用于反射Person.class
val mirror = ru.runtimeMirror(getClass.getClassLoader)
//得到Person类的Type对象后,得到type的特征值并转为ClassSymbol对象
val classPerson = ru.typeOf[Person].typeSymbol.asClass
//得到classMirror对象
val classMirror = mirror.reflectClass(classPerson)
//得到构造器Method
val constructor = ru.typeOf[Person].decl(ru.termNames.CONSTRUCTOR).asMethod
//得到MethodMirror
val methodMirror = classMirror.reflectConstructor(constructor)
//实例化该对象
val p = methodMirror(“Mike”, 1)
println§
}
}
16.10.3 运行时类成员的访问
class Person(name:String, age: Int) {
def myPrint() = {
println(name + “,” + age)
}
}
object PersonMain extends App{
override def main(args: Array[String]): Unit = {
//获取Environment和universe
val ru = scala.reflect.runtime.universe
//获取对应的Mirrors,这里是运行时的
val mirror = ru.runtimeMirror(getClass.getClassLoader)
//得到Person类的Type对象后,得到type的特征值并转为ClassSymbol对象
val classPerson = ru.typeOf[Person].typeSymbol.asClass
//用Mirrors去reflect对应的类,返回一个Mirrors的实例,而该Mirrors装载着对应类的信息
val classMirror = mirror.reflectClass(classPerson)
//得到构造器Method
val constructor = ru.typeOf[Person].decl(ru.termNames.CONSTRUCTOR).asMethod
//得到MethodMirror
val methodMirror = classMirror.reflectConstructor(constructor)
//实例化该对象
val p = methodMirror(“Mike”, 1)
println§
//反射方法并调用
val instanceMirror = mirror.reflect(p)
//得到Method的Mirror
val myPrintMethod = ru.typeOf[Person].decl(ru.TermName("myPrint")).asMethod
//通过Method的Mirror索取方法
val myPrint = instanceMirror.reflectMethod(myPrintMethod)
//运行myPrint方法
myPrint()
//得到属性Field的Mirror
val nameField = ru.typeOf[Person].decl(ru.TermName("name")).asTerm
val name = instanceMirror.reflectField(nameField)
println(name.get)
}
}