快学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)
  }
  ...
}
快学Scala第15章----注解_第1张图片
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的时候还是会抛出异常。

你可能感兴趣的:(快学Scala第15章----注解)