十三 注解
注解就是标签。
标签是用来标记某些代码需要特殊处理的。
处理的手段可以在代码运行时操作,也可以在编译期操作。
13.1 什么可以被注解
1) 可以为类,方法,字段局部变量,参数,表达式,类型参数以及各种类型定义添加注解
@Entity class Student @Test def play() {} @BeanProperty var username = _ def doSomething(@NotNull message: String) {} @BeanProperty @Id var username = _ |
2) 构造器注解,需要在主构造器之前,类名之后,且需要加括号,如果注解有参数,则写在注解括号里
class Student @Inject() (var username: String, var password: String) |
3) 为表达式添加注解,在表达式后添加冒号
(map1.get(key): @unchecked) match {...} |
4) 泛型添加注解
class Student[@specialized T] |
5) 实际类型添加注解
String @cps[Unit] |
13.2 注解参数
Java注解可以有带名参数:
@Test(timeout = 100, expected = classOf[IOException]) // 如果参数名为value,则该名称可以直接略去。 @Named("creds") var credentials: Credentials = _ // value参数的值为 “creds” // 注解不带参数,圆括号可以省去 @Entity class Credentials |
Java 注解的参数类型只能是:
数值型的字面量
字符串
类字面量
Java枚举
其他注解
上述类型的数组(但不能是数组的数组)
Scala注解可以是任何类型,但只有少数几个Scala注解利用了这个增加的灵活性。
13.3 注解实现
你可以实现自己的注解,但是更多的是使用Scala和Java提供的注解。
注解必须扩展Annotation特质:
class unchecked extends annotation.Annotation |
13.4 针对Java的注解
1) 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] |
2) 标记接口:Scala用注解@cloneable和@remote 而不是 Cloneable和Java.rmi.Remote“标记接口”来标记可被克隆的对象和远程的对象。
@cloneable class Employee |
3) 受检异常:和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会拒绝捕获该异常。
13.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 |
十四 类型参数(先了解即可)
14.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 Pair[Any, Any](42, "String") |
14.2 泛型函数
函数或方法也可以有类型(泛型)参数。
//
|
14.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(p) println(p.replaceFirst("Joke")) println(p) } } |
在Java中,T同时是A和B的子类型,称之为多界,形式如:<T extends A & B>。
在Scala中,对上界和下界不能有多个,但是可以使用混合类型,如:[T <: A with B]。
在Java中,不支持下界的多界形式。如:<T super A &B>这是不支持的。
在Scala中,对复合类型依然可以使用下界,如:[T >: A with B]。
14.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) } |
14.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) } } |
object ContextBoundsDemo {
|
14.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$Bar@271768ab
scala> val f2 = new Foo;val b2 = new f2.Bar f2: Foo = Foo@3e50039c b2: f2.Bar = Foo$Bar@771d16b9
scala> val ev1 = m(f1)(b1) warning: there were 2 deprecation warnings; re-run with -deprecation for details ev1: Manifest[f1.Bar] = Foo@681e731c.type#Foo$Bar
scala> val ev2 = m(f2)(b2) warning: there were 2 deprecation warnings; re-run with -deprecation for details ev2: Manifest[f2.Bar] = Foo@3e50039c.type#Foo$Bar
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))) } } |
14.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](x:T) = 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 ](i:T) = println("foo3 OK") foo3(2) |
14.9 类型约束
类型约束,提供了限定类型的另一种方式,一共有3中关系声明:
T =:= U意思为:T类型是否等于U类型
T <:< U意思为:T类型是否为U或U的子类型
T <%< U意思为:T类型是否被隐式(视图)转换为U
如果想使用上面的约束,需要添加“隐式类型证明参数”比如:
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 } } |
14.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]。 |
package UpperBoundsDemo01 |
14.11 上界(Upper Bounds)介绍和使用.
在 Java 泛型里表示某个类型是 A 类型的子类型,使用 extends 关键字,这种形式叫 upper
bounds(上限或上界),语法如下:
<T extendsA>
//或用通配符的形式:
<? extendsA>
scala 中上界
在 scala 里表示某个类型是 A 类型的子类型,也称上界或上限,使用 <: 关键字,语法如下:
[T <: A]
//或用通配符:
[_ <:A]十五 隐式转换和隐式参数
package UpperBoundsDemo01
|
14.12 下界(Upper Bounds)介绍和使用.
Java 中下界
在 Java 泛型里表示某个类型是 A 类型的父类型,使用 super 关键字
<T superA>
//或用通配符的形式:
<? superA>
scala 中下界
在 scala 的下界或下限,使用 >: 关键字,语法如下:
[T >: A]
//或用通配符:
[_ >:A]
package UpperBoundsDemo01
|