快学Scala第15章----注解

本章要点

  • 你可以为类、方法、字段、局部变量、参数、表达式、类型参数以及各种类型定义添加注解。
  • 对于表达式和类型,注解跟在被注解的条目之后
  • 注解的形式有: @Annotation、 @Annotation(value) 或 @Annotation(namel = value, ...)
  • @volatitle、 @transient、 @strictfp 和 @native 分别生成等效的Java修饰符。
  • 用@throws来生成与Java兼容的throws规格说明
  • @tailrec注解让你教研某个递归函数使用了尾递归优化
  • assert函数利用了@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注解是有区别的:
Java注解并不影响编译器如何将源码翻译成字节码;它们仅仅是往字节码中添加数据,以便外部工具可以利用到它们。而在Scala中,注解可以影响编译过程。例如@BeanPropetry注解将触发getter和setter方法(如果为var的话)的生成。


什么可以被注解

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

@Entity class Credentials
@Test def testSomeFeature() {}
@BeanProperty var username = _
def doSomething(@NotNull message: String) {}
// 同时添加多个注解
@BeanProperty @Id var username = _

// 给构造器添加注解,需要将注解放置在构造器之前,并加上一对圆括号(注解不带参数的话)
class Credentials @Inject() (var username: String, var password: String)

// 给表达式添加注解,需要在表达式后加上冒号,然后是注解本身
(myMap.get(key): @unchecked) match { ... }

// 为类型参数添加注解
class MyContainer[@specialized T]

// 为实际类型添加注解应放置在类型名称之后
String @cps[Unit]

注解参数

Java注解可以有带名参数:

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

// 如果参数名为value,则该名称可以直接略去。
@Named("creds") var credentials: Credentials = _  // value参数的值为 “creds”

// 注解不带参数,圆括号可以省去
@Entity class Credentials

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

  • 数值型的字面量
  • 字符串
  • 类字面量
  • Java枚举
  • 其他注解
  • 上述类型的数组(但不能是数组的数组)

Scala注解可以是任何类型,但只有少数几个Scala注解利用了这个增加的灵活性。


注解实现

你可以实现自己的注解,但是更多的是使用Scala和Java提供的注解。
注解必须扩展Annotation特质:

class unchecked extends annotation.Annotation

针对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]
  1. 标记接口
    Scala用注解@cloneable和@remote 而不是 Cloneable和Java.rmi.Remote标记接口来标记可被克隆的和远程的对象。
@cloneable class Employee

对于可序列化的类,你可以用@SerialVersionUID注解来指定序列化版本

@SerialVersionUID(6157032470129070425L)
class Employee extends Person with Serializable
  1. 受检异常
    和Scala不同,Java编译器会跟踪受检异常。如果你从Java代码中调用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,否则会拒绝捕获该异常。

  1. 变长参数
    @varargs注解让你可以从Java调用Scala的带有变长参数的方法。
// 默认情况
def process(agrs: String*)
// Scala编译器会把变长参数翻译成序列:
def process(args: Seq[String])   // 这样的方法签名在Java中使用很费劲

// 加上 @varargs
@varargs def process(args: String*)
// 编译器将生成如下java方法
void process(String... args)
  1. JavaBeans
    @BeanProperty注解将会生成JavaBean风格的getter和setter方法。

用于优化的注解

  1. 尾递归
    递归调用有时候能被转化成循环,这样能节约栈空间。
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注解。
注意上面的方法是在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)
  }
  ...
}
tailrec.png

这种情况下,你可以将方法挪到对象中,或者将它声明为private或final。

**说明: **对于消除递归,一个更加通用的机制叫做“蹦床”。蹦床的实现会将执行一个循环,不停的调用函数。每个函数都返回下一个将被调用的函数。尾递归在这里是一个特例,每个函数都返回它自己。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

2 跳转表生成与内联
在C++或Java中,switch语句通常被编译成跳转表,用于多种情况的判断,比if/else方便高效。Scala也会尝试对匹配语句生成跳转表,使用@switch注解。

(n @switch) match {
  case 0 => "Zero"
  case 1 => "One"
  case _ => "?"
}

另一个常见的优化方法是使用@inline来进行内联:用函数体替换函数调用,这与C++或Java的inline函数相同。 而@noinline来告诉编译器不要内联。

  1. 可省略方法
    @elidable注解给那些可以在生产代码中移除的方法打上标记。
@elidable(500) deg dump(props: Map[String, String]) = { ... }

使用 scalac -Xelide-below 800 myprog.scala 则上述方法代码不会被生成。

  1. 基本类型的特殊化
    打包和解包基本类型的值是不高效的,但是在泛型代码中很常见。
def allDifferent[T](x: T, y: T, z: T) = x != y && x != x && y != z

当你调用allDifferent(3,4,5) 则参数的类型为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注解用于某个隐士参数不存在的时候生成有意义的错误提示。
@unchecked注解用于匹配不完整时取消警告信息:

(lst: @unchecked) match {
  case head :: tail => ...
}

编译器不会报告说没有给出Nil选项。但是当lst是Nil的时候还是会抛出异常。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容