本章要點
- 你可以為類、方法、字段、局部變量、參數(shù)、表達式、類型參數(shù)以及各種類型定義添加注解。
- 對于表達式和類型,注解跟在被注解的條目之后
- 注解的形式有: @Annotation、 @Annotation(value) 或 @Annotation(namel = value, ...)
- @volatitle、 @transient、 @strictfp 和 @native 分別生成等效的Java修飾符。
- 用@throws來生成與Java兼容的throws規(guī)格說明
- @tailrec注解讓你教研某個遞歸函數(shù)使用了尾遞歸優(yōu)化
- assert函數(shù)利用了@elidable注解。你可以選擇從Scala程序中移除所有斷言。
- 用@deprecated注解來標記已過時的特性。
什么是注解
注解是那些你插入到代碼中,以便有工具可以對它們進行處理的標簽。工具可以在代碼級別運作,也可以處理被編譯器加入了注解信息的類文件。
注解的語法和Java一樣:
@Test(timeout = 100) def testSomeFeature() { ... }
@Entity class Credentials {
@Id @BeanProperty var username: String = _
@BeanProperty var password: String = _
}
你可以對Scala類使用Java注解。上述示例中的注解除了@BeanProperty外,其他的都來自JUnit和JPA,而這兩個Java框架并不知道我們用的是Scala。
Scala特有的注解通常是由Scala編譯器或編譯器插件處理。Scala注解和Java注解是有區(qū)別的:
Java注解并不影響編譯器如何將源碼翻譯成字節(jié)碼;它們僅僅是往字節(jié)碼中添加數(shù)據(jù),以便外部工具可以利用到它們。而在Scala中,注解可以影響編譯過程。例如@BeanPropetry注解將觸發(fā)getter和setter方法(如果為var的話)的生成。
什么可以被注解
在Scala中,你可以為類、方法、字段、局部變量和參數(shù)添加注解
@Entity class Credentials
@Test def testSomeFeature() {}
@BeanProperty var username = _
def doSomething(@NotNull message: String) {}
// 同時添加多個注解
@BeanProperty @Id var username = _
// 給構造器添加注解,需要將注解放置在構造器之前,并加上一對圓括號(注解不帶參數(shù)的話)
class Credentials @Inject() (var username: String, var password: String)
// 給表達式添加注解,需要在表達式后加上冒號,然后是注解本身
(myMap.get(key): @unchecked) match { ... }
// 為類型參數(shù)添加注解
class MyContainer[@specialized T]
// 為實際類型添加注解應放置在類型名稱之后
String @cps[Unit]
注解參數(shù)
Java注解可以有帶名參數(shù):
@Test(timeout = 100, expected = classOf[IOException])
// 如果參數(shù)名為value,則該名稱可以直接略去。
@Named("creds") var credentials: Credentials = _ // value參數(shù)的值為 “creds”
// 注解不帶參數(shù),圓括號可以省去
@Entity class Credentials
Java 注解的參數(shù)類型只能是:
- 數(shù)值型的字面量
- 字符串
- 類字面量
- Java枚舉
- 其他注解
- 上述類型的數(shù)組(但不能是數(shù)組的數(shù)組)
Scala注解可以是任何類型,但只有少數(shù)幾個Scala注解利用了這個增加的靈活性。
注解實現(xiàn)
你可以實現(xiàn)自己的注解,但是更多的是使用Scala和Java提供的注解。
注解必須擴展Annotation特質(zhì):
class unchecked extends annotation.Annotation
針對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
對于可序列化的類,你可以用@SerialVersionUID注解來指定序列化版本
@SerialVersionUID(6157032470129070425L)
class Employee extends Person with Serializable
- 受檢異常
和Scala不同,Java編譯器會跟蹤受檢異常。如果你從Java代碼中調(diào)用Scala的方法,其簽名應包含那些可能被拋出的受檢異常。用@throws注解來生成正確的簽名。
class Book {
@throws (classOf[IOException]) def read(filename: String) { ... }
// Java版本為
// void read(String filename) throws IOException
...
}
// 如果沒有@throws注解,Java代碼將不能捕獲該異常
try {
book.read("war-and-peace.txt");
} catch (IOException ex) {
...
}
Java 編譯器需要知道read方法可以拋出IOException,否則會拒絕捕獲該異常。
- 變長參數(shù)
@varargs注解讓你可以從Java調(diào)用Scala的帶有變長參數(shù)的方法。
// 默認情況
def process(agrs: String*)
// Scala編譯器會把變長參數(shù)翻譯成序列:
def process(args: Seq[String]) // 這樣的方法簽名在Java中使用很費勁
// 加上 @varargs
@varargs def process(args: String*)
// 編譯器將生成如下java方法
void process(String... args)
- JavaBeans
@BeanProperty注解將會生成JavaBean風格的getter和setter方法。
用于優(yōu)化的注解
- 尾遞歸
遞歸調(diào)用有時候能被轉(zhuǎn)化成循環(huán),這樣能節(jié)約??臻g。
object Util {
def sum(xs: Seq[Int]): BigInt = {
if (xs.isEmpty) 0 else xs.head + sum(xs.tail)
}
...
}
上面的sum方法無法被優(yōu)化,因為計算過程中最后一步是加法,而不是遞歸調(diào)用。調(diào)整后的代碼:
def sum2(xs: Seq[Int], partial: BigInt): BigInt = {
if (xs.isEmpty) partial else sum2(xs.tail, xs.head + partial)
}
Scala編譯器會自動對sum2應用“尾遞歸”優(yōu)化。如果你調(diào)用sum(1 to 1000000) 將會發(fā)生一個棧溢出錯誤。不過sum2(1 to 1000000, 0) 將會得到正確的結果。
盡管Scala編譯器會嘗試使用尾遞歸優(yōu)化,但有時候某些不太明顯的原因會造成它無法這樣做。如果你想編譯器無法進行優(yōu)化時報錯,則應該給你的方法加上@tailrec注解。
注意上面的方法是在Object中定義的,如果是在class中呢:
class Util {
import scala.annotation._
@tailrec def sum2(xs: Seq[Int], partial: BigInt): BigInt = {
if (xs.isEmpty) partial else sum2(xs.tail, xs.head + partial)
}
...
}

這種情況下,你可以將方法挪到對象中,或者將它聲明為private或final。
**說明: **對于消除遞歸,一個更加通用的機制叫做“蹦床”。蹦床的實現(xiàn)會將執(zhí)行一個循環(huán),不停的調(diào)用函數(shù)。每個函數(shù)都返回下一個將被調(diào)用的函數(shù)。尾遞歸在這里是一個特例,每個函數(shù)都返回它自己。Scala有一個名為TailCalls的工具對象,幫助我們輕松實現(xiàn)蹦床。
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
2 跳轉(zhuǎn)表生成與內(nèi)聯(lián)
在C++或Java中,switch語句通常被編譯成跳轉(zhuǎn)表,用于多種情況的判斷,比if/else方便高效。Scala也會嘗試對匹配語句生成跳轉(zhuǎn)表,使用@switch注解。
(n @switch) match {
case 0 => "Zero"
case 1 => "One"
case _ => "?"
}
另一個常見的優(yōu)化方法是使用@inline來進行內(nèi)聯(lián):用函數(shù)體替換函數(shù)調(diào)用,這與C++或Java的inline函數(shù)相同。 而@noinline來告訴編譯器不要內(nèi)聯(lián)。
- 可省略方法
@elidable注解給那些可以在生產(chǎn)代碼中移除的方法打上標記。
@elidable(500) deg dump(props: Map[String, String]) = { ... }
使用 scalac -Xelide-below 800 myprog.scala 則上述方法代碼不會被生成。
- 基本類型的特殊化
打包和解包基本類型的值是不高效的,但是在泛型代碼中很常見。
def allDifferent[T](x: T, y: T, z: T) = x != y && x != x && y != z
當你調(diào)用allDifferent(3,4,5) 則參數(shù)的類型為java.lang.Integer。你可以重載該版本,指定具體的類型,你也可以讓編譯器自動生成這些方法,使用@specialized注解:
def allDifferent[@specialized T](x: T, y: T, z: T) = ...
// 你也可以將特化限定在某幾個可選類型的子集
def allDifferent[@specialized(Long, Double) T](x: T, y: T, z: T) = ...
用于錯誤和警告的注解
如果給特性加上@deprecated注解,則每當編譯器遇到這個特性的使用時都會生成一個警告信息。
@deprecated(message = "Use factorial(n: BigInt) instead")
def factorial(n: Int): Int = ...
@implicitNotFound注解用于某個隱士參數(shù)不存在的時候生成有意義的錯誤提示。
@unchecked注解用于匹配不完整時取消警告信息:
(lst: @unchecked) match {
case head :: tail => ...
}
編譯器不會報告說沒有給出Nil選項。但是當lst是Nil的時候還是會拋出異常。