什么可以被注解
- 在scala中,可以为类、方法、字段、局部变量和参数添加注解,例如:
@Entity class Credentials
@Test def testSomeFeature() {}
@BeanProperty var username = _
def doSomething(@NotNull message: String) {}
- 在给主构造器添加注解时,需要将注解放置在构造器之前,并加上一对圆括号(如果注解不带参数的话)。例如:
class Credentials @Inject() (var username: String, var password: String)
- 你还可以为表达式添加注解;需要在表达式后加上冒号,然后是注解本身,例如:
(myMap.get(key): @unchecked) match { ... } // 为表达式myMap.get(key)添加了注解
- 可以为类型参数添加注解,例如:
class MyContainer[@specialized T]
- 针对实际类型的注解应放置在类型名称之后,就像这样:
def country: String @Localized // 为String类型添加了注解, 该方法返回的是一个本地化了的字符串
注解参数
Java注解的参数类型只能是:数值型的字面量、字符串、类字面量、Java枚举、其他注解、上述类型的数组(但不能是数组的数组)。
scala注解的参数可以是任何类型;举例来说,@deprecatedName注解有一个类型为Symbol的参数。
注解实现
注解必须扩展Annotation特质,例如,unchecked注解定义如下:
class unchecked extends annotation.Annotation
类型注解必须扩展自TypeAnnotation特质,如下:
class Localized extends StaticAnnotation with TypeConstraint
默认情况下,构造器参数注解仅会被应用到参数自身,而字段注解只能应用到字段;元注解@param、@field、@getter、@setter、@beanGetter和@beanSetter
将使得注解被附在别处。
Java修饰符
对于那些不是很常用的Java特性,Scala使用注解而不是修饰符关键字。
- @volatile注解将宇段标记为易失的:
@volatile var done = false // 在JVM中将成为volatile的字段
一个易失的字段可以被多个线程同时更新。
2. @transient注解将字段标记为瞬态的:
@transient var recentLookups = new HashMap[String, String] // 在JVM中将成为transient的字段
瞬态的字段不会被序列化;这对于需要临时保存的缓存数据,或者能够很容易地重新计算的数据而言是合理的。
3. @strictfp注解对应Java中的strictfp修饰符:
@strictfp def calculate(x: Double) = ...
该方法使用IEEE的double值来进行浮点运算,而不是使用80位扩展精度( Intel处理器默认使用的实现)。其计算结果会更慢,但代码可移植性更高。
4. @native注解用来标记那些在C和C++代码中实现的方法。其对应Java中的native修饰符。
@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代码中调用Scala的方法,其签名应包含那些可能被抛出的受检异常。@throws注解来生成正确的签名。例如:
class Book{
@throws(classOf[IOException]) def read(filename: String) { ... }
...
}
Java版的签名为:
void read(String filename) throws IOException
如果没有@throws注解,Java代码将不能捕获该异常。
try { // 这是Java代码
book.read("xxxxxx.txt");
} catch(IOException ex) {
...
}
Java编译器需要知道read方法可以抛IOException,否则会拒绝捕获该异常。
变长参数
@varargs注解让你可以从Java调用Scala的带有变长参数的方法。默认情况下,如果给出如下方法:
def process(args: String*)
Scala编译器将会把变长参数翻译成序列:
def process(args: Seq[String])
这样的方法签名在Java中使用起来很费劲,如果你加上@varargs:
@varargs def process(args: String*)
则编译器将生成如下Java方法:
void process(String... args) // Java桥接方法
该方法将args数组包装在Seq中,然后调用那个实际的Scala方法。
JavaBeans
如果给字段添加上@scala.reflect.BeanProperty注解,编译器将生成JavaBeans风格的getter和setter方法, 例如:
class Person{
@BeanProperty var name: String = _
}
上述定义除了生成Scala版的getter和setter,还将生成如下方法:
getName(): String
setName(newValue: String): Unit
@BooleanBeanProperty对类型为Boolean的字段生成带有is前缀的getter方法。
用于优化的注解
Scala类库中的有些注解可以控制编译器优化。
尾递归
递归调用有时能被转化成循环,这样能节约栈空间。在函数式编程中,这是很重要的,我们通常会使用递归方法来遍历集合。考虑如下用递归计算整数序列之和的方法:
object Util{
def sum(xs: Seq[Int]): BigInt =
if (xs.isEmpty) 0 else xs.head + sum(xs.tail)
...
}
该方法无法被优化,因为计算过程的最后一步是加法,而不是递归调用。不过,其稍微调整变换一下就可以被优化了:
def sum2(xs: Seq[Int] partial: Bigint): Bigint =
if(xs.isEmpty) partial else sum2(xs.tail, xs.head + partial)
部分和(partial sum)被作为参数传递;用sum2(xs, 0)的方式调用该方法。由于计算过程的最后一步是递归地调用同一个方法,因此它可以被变换成跳回到方法顶部的循环。Scala编译器会自动对第二个方法应用“尾递归”优化。如果你调用sum (1 to 1000000)
,就会得到一个栈溢出错误(至少对于默认栈大小的 JVM而言如此);不过,sum2(1 to 1000000, 0)
,将返回序列之和500000500000。
尽管Scala编译器会尝试使用尾递归优化,但有时候某些不太明显的原因会造成它无法这样做。如果你有赖于编译器来帮你去掉递归,则应该给你的方法加上@tailrec注解。这样一来,如果编译器无法应用该优化,它就会报错。
举例来说,假定sum2方法位于某个类而不是某个对象当中:
class Util {
@tailrec def sum2(xs: Seq[Int], partial: Bigint): Bigint =
if (xs.isEmpty) partial else sum2(xs.tail, xs.head + partial)
...
}
现在这个程序编译就会失败,错误提示为” could not opt mize @tailrec annotated method sum2: it is neither private nor final so can be override 在这种情况下,可以将方法挪到对象中,或者也可以将它声明为private和final。
说明:对于消除递归,一个更加通用的机制叫作“蹦床”。蹦床的实现会执行一个循环 ,不停地调用函数。 每一个函数都返回下一个将被调用的函数。尾递归在这里是一个特例,每个函数都返回它自己。而更通用的版本允许相互调用。
Scala有一个名为TailCalls的工具对象,帮助我们轻松地实现蹦床。相互递归的函数返回类型为TailRec[A],其要么返回done(result),要么返回tailcall(fun)。其中,fun是下一个被调用的函数。这必须是一个不带额外参数且同样返回TailRec[A]的函数。以下是一个简单的示例:
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
跳转表生成与内联
在C++或Java中,switch语句通常可以被编译成跳转表(jump table),这比起一系列的if/else表达式要更加高效。Scala也会尝试对匹配语句生成跳转表。@switch注解让你检查Scala的match语句是不是真的被编译成了跳转表。将注解应用到match语句前的表达式:
(n: @switch) match {
case 0 => "Zero"
case 1 => "One"
case _ => "?"
}
另一个常见的优化是方法内联(inlining):将方法调用语句替换为被调用的方法体,可以将方法标记为@inline来建议编译器做内联,或标记为@noinline来告诉编译器不要内联。通常,内联的动作发生在JVM内部,它的“即时”编译器无须我们用注解告诉它该怎么做,也能有很好的效果。你可以用@inline和@noinline来告诉Scala编译器要不要内联(如果你认为有这个必要的话)。
可省略方法
@elidable注解给那些可以在生产代码中移除的方法打上标记。例如:
@elidable(500) def dump(props: Map[String, String]) { ... }
如果你用如下命令编译:
scalac -Xelide-below 800 myprog.scala
则上述方法代码不会被生成。elidable对象定义了如下数值常量:
- MAXIMUM 或 OFF = Int.MaxValue
- ASSERTION = 2000
- SEVERE = 1000
- WARNING = 900
- INFO = 800
- CONFIG = 700
- FINE = 500
- FINER = 400
- FINEST = 300
- MINIMUM 或 ALL = Int.MinValue
你可以在注解中使用这些常量:
import scala.annotation.elidable._
@elidable(FINE) def dump(props: Map[String, String]){ ... }
也可以在命令行中使用这些名称:
scalac -Xelide-below INFO myprog.scala
如果不指定-Xelide-below标志,那些被注解的值低于1000 的方法会被省略,剩下SEVERE的方法和断言,但会去掉所有警告。
说明:ALL和OFF级别可能会让人感到困惑。注解@elide(ALL)表示方法总是被省略,而@elide(OFF)表示方法永不被省略。但-Xelide-below OFF 的意思是要省略所有方法,而-Xelide-below ALL的意思是什么都不要省略。这就是后来又增加了MAXIMUM和MINIMUM的原因。
Predef模块定义了一个可被忽略的assert方法。例如,我们可以写:
def makeMap(keys: Seq[String], values: Seq[String]) = {
assert(keys.length == values.length, "lengths don't match")
...
}
如果我们用不匹配的两个参数来调用该方法,则assert方法将抛出AssertionError,报错消息为“assertion failed: lengths don’t match”。
如果要禁用断言,可以用-Xelide-below 2001或-Xelide-below MAXIMUM。注意在默认情况下断言不会被禁用。与Java断言相比,这是一个受欢迎的改进。
注意:对被省略的方法调用,编译器会帮我们替换成Unit对象。如果用到了被省略方法的返回值,则一个ClassCastException会被抛出。最好只对那些没有返回值的方法使用@elidable注解。
基本类型的特殊化
打包和解包基本类型的值是不高效的,但在泛型代码中这很常见。考虑如下示例:
def allDifferent[T](x: T, y: T, z: T) = x != y && x != z && y != z
如果你调用allDifferent(3, 4, 5),在方法被调用之前,每个整数值都被包装成一个java.lang.Integer。当然了,我们可以给出一个重载的版本
def allDifferent(x: Int, y: Int, z: Int) = ...
以及其他七个方法,分别对应其他基本类型。
可以让编译器自动生成这些方法,具体做法就是给类型参数添加@specialized注解:
def allDifferent[@specialized T](x: T, y: T, z: T) = ...
也可以限定只对某个类型子集做特殊化:
def allDifferent[@specialized(Long, Double) T] (x: T, y: T, z: T) = ...
在注解构造器中,可以指定如下类型的任意子集:Unit、Boolean、Byte、Short、Char、Int、Long、Float、Double。
用于错误和警告的注解
如果你给某个特性加上了@deprecated注解,则每当编译器遇到对这个特性的使用时都会生成一个警告信息,该注解有两个选填参数:message和since。
@deprecated(message = "Use factorial(n:Bigint) instead")
def factorial(n: Int): Int = ...
@deprecatedName可以被应用到参数上,并给出一个该参数之前使用过的名称。
def draw(@deprecatedName('sz) size: Int, style: Int = NORMAL)
你仍然可以调用draw(sz = 12),不过你将会得到一个表示该名称已过时的警告。
说明:这里的构造器参数是一个符号(symbol):以单引号开头的名称。名称相同的符号一定是唯一的。从这个意义上讲,符号比字符串的效率要高一些。更重要的是,在语义上两者有着显著区别:符号表示的是程序中某个项目的名称。
@deprecatedInheritance和@deprecatedOverriding注解将针对从某个类继承或重写某个方法的情况生成过期警告(译者注:意思是对应的类或方法可能在未来的版本中改成final)。
@implicitNotFound和@implicitAmbiguous注解用于在某个隐式参数不存在或不明确的时候生成有意义的错误提示。
@unchecked注解用于在匹配不完整时取消警告信息。举例来说,假定我们知道某个列表不可能是空的:
(lst: @unchecked) match{
case head :: tail =>
...
}
编译器不会报告说我没给出Nil选项。当然了,如果lst的确是Nil,则在运行期会抛异常。
@uncheckedVariance注解会取消与型变相关的错误提示;举例来说,Java.util.Comparator按理应该是逆变的,如果Student是Person的子类型,那么在需要Comparator[Student]时,我们也可以用Comparator[Person]。但是,Java的泛型不支持型变,我们可以通过@uncheckedVariance注解来解决这个问题:
trait Comparator[-T] extends java.lang.Comparator[T @uncheckedVariance]
参考:快学scala(第二版)