Topics in This Chapter
15.4 Annotation Implementations
15.5 Annotations for Java Features
Java Modifiers. Marker Interfaces. Checked Exceptions.
Variable Arguments. JavaBeans.
15.6 Annotations for Optimizations
Tail Recursion. Jump Table Generation and Inlining.
Eliding Methods. Specialization for Primitive Types.
15.7 Annotations for Errors and Warnings
Annotations let you add information to program items. This information can be processed by the compiler or by external tools. In this chapter, you will learn how to interoperate with Java annotations and how to use the annotations that are specific to Scala.
The key points of this chapter are:
• You can annotate classes, methods, fields, local variables, parameters, expressions, type parameters, and types.
• With expressions and types, the annotation follows the annotated item.
• Annotations have the form @Annotation
, @Annotation(value)
, or @Annotation(name1 = value1, ...)
.
• @volatile
, @transient
, @strictfp
, and @native
generate the equivalent Java modifiers.
• Use @throws
to generate Java-compatible throws
specifications.
• The @tailrec
annotation lets you verify that a recursive function uses tail call optimization.
• The assert
function takes advantage of the @elidable
annotation. You can optionally remove assertions from your Scala programs.
• Use the @deprecated
annotation to mark deprecated features.
Annotations are tags that you insert into your source code so that some tools can process them. These tools can operate at the source level, or they can process the class files into which the compiler has placed your annotations.
Annotations are widely used in Java, for example by testing tools such as JUnit 4 and enterprise technologies such as Java EE.
The syntax is just like in Java. For example:
@Test(timeout = 100) def testSomeFeature() { ... }
@Entity class Credentials {
@Id @BeanProperty var username : String = _
@BeanProperty var password : String = _
}
You can use Java annotations with Scala classes. The annotations in the preceding examples are from JUnit and JPA, two Java frameworks that have no particular knowledge of Scala.
You can also use Scala annotations. These annotations are specific to Scala and are usually processed by the Scala compiler or a compiler plugin. (Implementing a compiler plugin is a nontrivial undertaking that is not covered in this book.)
Java annotations do not affect how the compiler translates source code into bytecode; they merely add data to the bytecode that can be harvested by external tools. In Scala, annotations can affect the compilation process. For example, the @BeanProperty
annotation that you saw in Chapter 5 causes the generation of getter and setter methods.
In Scala, you can annotate classes, methods, fields, local variables, and parameters, just like in Java.
@Entity class Credentials
@Test def testSomeFeature() {}
@BeanProperty var username = _
def doSomething(@NotNull message: String) {}
You can apply multiple annotations. The order doesn’t matter.
@BeanProperty @Id var username = _
When annotating the primary constructor, place the annotation before the constructor, and add a pair of parentheses if the annotation has no arguments.
class Credentials @Inject() (var username: String, var password: String)
You can also annotate expressions. Add a colon followed by the annotation, for example:
(myMap.get(key): @unchecked) match { ... }
// The expression myMap.get(key) is annotated
You can annotate type parameters:
class MyContainer[@specialized T]
Annotations on an actual type are placed after the type, like this:
def country: String @Localized
Here, the String
type is annotated. The method returns a localized string.
Java annotations can have named arguments, such as
@Test(timeout = 100, expected = classOf[IOException])
However, if the argument name is value
, it can be omitted. For example:
@Named("creds") var credentials: Credentials = _
// The value argument is "creds"
If the annotation has no arguments, the parentheses can be omitted:
@Entity class Credentials
Most annotation arguments have defaults. For example, the timeout
argument of the JUnit @Test
annotation has a default value of 0
, indicating no timeout. The expected
argument has as default a dummy class to signify that no exception is expected. If you use
@Test def testSomeFeature() { ... }
this annotation is equivalent to
@Test(timeout = 0, expected = classOf[org.junit.Test.None])
def testSomeFeature() { ... }
Arguments of Java annotations are restricted to the following types:
• Numeric literals
• Strings
• Class literals
• Java enumerations
• Other annotations
• Arrays of the above (but not arrays of arrays)
Arguments of Scala annotations can be of arbitrary types, but only a couple of the Scala annotations take advantage of this added flexibility. For instance, the @deprecatedName
annotation has an argument of type Symbol
.
I don’t expect that many readers of this book will feel the urge to implement their own Scala annotations. The main point of this section is to be able to decipher the implementation of the existing annotation classes.
An annotation must extend the Annotation
trait. For example, the unchecked
annotation is defined as follows:
class unchecked extends annotation.Annotation
A type annotation must extend the TypeAnnotation
trait:
class Localized extends StaticAnnotation with TypeConstraint
Caution
If you want to implement a new Java annotation, you need to write the annotation class in Java. You can, of course, use that annotation for your Scala classes.
Generally, an annotation describes the expression, variable, field, method, class, or type to which it is applied. For example, the annotation
def check(@NotNull password: String)
applies to the parameter variable password
.
However, field definitions in Scala can give rise to multiple features in Java, all of which can potentially be annotated. For example, consider
class Credentials(@NotNull @BeanProperty var username: String)
Here, there are six items that can be annotation targets:
• The constructor parameter
• The private instance field
• The accessor method username
• The mutator method username_=
• The bean accessor getUsername
• The bean mutator setUsername
By default, constructor parameter annotations are only applied to the parameter itself, and field annotations are only applied to the field. The meta-annotations
@param
, @field
, @getter
, @setter
, @beanGetter
, and @beanSetter
cause an annotation to be attached elsewhere. For example, the @deprecated
annotation is defined as:
@getter @setter @beanGetter @beanSetter
class deprecated(message: String = "", since: String = "")
extends annotation.StaticAnnotation
You can also apply these annotations in an ad-hoc fashion:
@Entity class Credentials {
@(Id @beanGetter) @BeanProperty var id = 0
...
}
In this situation, the @Id
annotation is applied to the Java getId
method, which is a JPA requirement for property access.
The Scala library provides annotations for interoperating with Java. They are presented in the following sections.
Scala uses annotations instead of modifier keywords for some of the less commonly used Java features.
The @volatile
annotation marks a field as volatile:
@volatile var done = false // Becomes a volatile field in the JVM
A volatile field can be updated in multiple threads.
The @transient
annotation marks a field as transient:
@transient var recentLookups = new HashMap[String, String]
// Becomes a transient field in the JVM
A transient field is not serialized. This makes sense for cache data that need not be saved, or data that can easily be recomputed.
The @strictfp
annotation is the analog of the Java strictfp
modifier:
@strictfp def calculate(x: Double) = ...
This method does its floating-point calculations with IEEE double
values, not using the 80 bit extended precision (which Intel processors use by default). The result is slower and less precise but more portable.
The @native
annotation marks methods that are implemented in C or C++ code. It is the analog of the native
modifier in Java.
@native def win32RegKeys(root: Int, path: String): Array[String]
Scala uses annotations @cloneable
and @remote
instead of the Cloneable
and java.rmi.Remote
marker interfaces for cloneable and remote objects.
@cloneable class Employee
With serializable classes, you can use the @SerialVersionUID
annotation to specify the serial version:
@SerialVersionUID(6157032470129070425L)
class Employee extends Person with Serializable
Note
For more information about Java concepts such as volatile fields, cloning, or serialization, see C. Horstmann, Core Java®, Tenth Edition (Prentice Hall, 2016).
Unlike Scala, the Java compiler tracks checked exceptions. If you call a Scala method from Java code, its signature should include the checked exceptions that can be thrown. Use the @throws
annotation to generate the correct signature. For example,
class Book {
@throws(classOf[IOException]) def read(filename: String) { ... }
...
}
The Java signature is
void read(String filename) throws IOException
Without the @throws
annotation, the Java code would not be able to catch the exception.
try { // This is Java
book.read("war-and-peace.txt");
} catch (IOException ex) {
...
}
The Java compiler needs to know that the read
method can throw an IOException
, or it will refuse to catch it.
The @varargs
annotation lets you call a Scala variable-argument method from Java. By default, if you supply a method such as
def process(args: String*)
the Scala compiler translates the variable argument into a sequence
def process(args: Seq[String])
That is cumbersome to use in Java. If you add @varargs
,
@varargs def process(args: String*)
then a Java method
void process(String... args) // Java bridge method
is generated that wraps the args
array into a Seq
and calls the Scala method.
You have seen the @BeanProperty
annotation in Chapter 5. When you annotate a field with @scala.reflect.BeanProperty
, the compiler generates JavaBeans-style getter and setter methods. For example,
class Person {
@BeanProperty var name : String = _
}
generates methods
getName() : String
setName(newValue : String) : Unit
in addition to the Scala getter and setter.
The @BooleanBeanProperty
annotation generates a getter with an is
prefix for a Boolean method.
Note
The annotations @BeanDescription
, @BeanDisplayName
, @BeanInfo
, @BeanInfoSkip
let you control some of the more obscure features of the JavaBeans specifications. Very few programmers need to worry about these. If you are among them, you’ll figure out what to do from the Scaladoc descriptions.
Several annotations in the Scala library let you control compiler optimizations. They are discussed in the following sections.
A recursive call can sometimes be turned into a loop, which conserves stack space. This is important in functional programming where it is common to write recursive methods for traversing collections.
Consider this method that computes the sum of a sequence of integers using recursion:
object Util {
def sum(xs: Seq[Int]): BigInt =
if (xs.isEmpty) 0 else xs.head + sum(xs.tail)
...
}
This method cannot be optimized because the last step of the computation is addition, not the recursive call. But a slight transformation can be optimized:
def sum2(xs: Seq[Int], partial: BigInt): BigInt =
if (xs.isEmpty) partial else sum2(xs.tail, xs.head + partial)
The partial sum is passed as a parameter; call this method as sum2(xs, 0)
. Since the last step of the computation is a recursive call to the same method, it can be transformed into a loop to the top of the method. The Scala compiler automatically applies the “tail recursion” optimization to the second method. If you try
sum(1 to 1000000)
you will get a stack overflow error (at least with the default stack size of the JVM), but
sum2(1 to 1000000, 0)
returns the sum 500000500000
.
Even though the Scala compiler will try to use tail recursion optimization, it is sometimes blocked from doing so for nonobvious reasons. If you rely on the compiler to remove the recursion, you should annotate your method with @tailrec
. Then, if the compiler cannot apply the optimization, it will report an error.
For example, suppose the sum2
method is in a class instead of an object:
class Util {
@tailrec def sum2(xs: Seq[Int], partial: BigInt): BigInt =
if (xs.isEmpty) partial else sum2(xs.tail, xs.head + partial)
...
}
Now the program fails with an error message "could not optimize @tailrec annotated method sum2: it is neither private nor final so can be overridden"
. In this situation, you can move the method into an object, or you can declare it as private
or final
.
Note
A more general mechanism for recursion elimination is “trampolining”. A trampoline implementation runs a loop that keeps calling functions. Each function returns the next function to be called. Tail recursion is a special case where each function returns itself. The more general mechanism allows for mutual calls—see the example that follows.
Scala has a utility object called TailCalls
that makes it easy to implement a trampoline. The mutually recursive functions have return type TailRec[A]
and return either done(result)
or tailcall(fun)
where fun
is the next function to be called.This needs to be a parameterless function that also returns a TailRec[A]
. Here is a simple example:
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))
To obtain the final result from the TailRec
object, use the result
method:
evenLength(1 to 1000000).result
In C++ or Java, a switch
statement can often be compiled into a jump table, which is more efficient than a sequence of if
/else
expressions. Scala attempts to generate jump tables for match clauses as well. The @switch
annotation lets you check whether a Scala match
clause is indeed compiled into one. Apply the annotation to the expression preceding a match
clause:
(n: @switch) match {
case 0 => "Zero"
case 1 => "One"
case _ => "?"
}
A common optimization is method inlining—replacing a method call with the method body. You can tag methods with @inline
to suggest inlining, or @noinline
to suggest not to inline. Generally, inlining is done in the JVM, whose “just in time” compiler does a good job without any annotations. The @inline
and @noinline
annotations let you direct the Scala compiler, in case you perceive the need to do so.
The @elidable
annotation flags methods that can be removed in production code. For example,
@elidable(500) def dump(props: Map[String, String]) { ... }
If you compile with
scalac -Xelide-below 800 myprog.scala
then the method code will not be generated. The elidable
object defines the following numerical constants:
• MAXIMUM
or OFF = Int.MaxValue
• ASSERTION = 2000
• SEVERE = 1000
• WARNING = 900
• INFO = 800
• CONFIG = 700
• FINE = 500
• FINER = 400
• FINEST = 300
• MINIMUM
or ALL = Int.MinValue
You can use one of these constants in the annotation:
import scala.annotation.elidable._
@elidable(FINE) def dump(props: Map[String, String]) { ... }
You can also use these names in the command line:
scalac -Xelide-below INFO myprog.scala
If you don’t specify the -Xelide-below
flag, annotated methods with values below 1000
are elided, leaving SEVERE
methods and assertions, but removing warnings.
The levels ALL
and OFF
are potentially confusing. The annotation @elide(ALL)
means that the method is always elided, and @elide(OFF)
means that it is never elided. But -Xelide-below OFF
means to elide everything, and -Xelide-below ALL
means to elide nothing. That’s why MAXIMUM
and MINIMUM
have been added.
The Predef
object defines an elidable assert
method. For example,
def makeMap(keys: Seq[String], values: Seq[String]) = {
assert(keys.length == values.length, "lengths don't match")
...
}
If the method is called with mismatched arguments, the assert
method throws an AssertionError
with message assertion failed: lengths don’t match
.
To disable assertions, compile with -Xelide-below 2001
or -Xelide-below MAXIMUM
. Note that by default assertions are not disabled. This is a welcome improvement over Java assertions.
Caution
Calls to elided methods are replaced with Unit
objects. If you use the return value of an elided method, a ClassCastException
is thrown. It is best to use the @elidable
annotation only with methods that don’t return a value.
It is inefficient to wrap and unwrap primitive type values—but in generic code, this often happens. Consider, for example,
def allDifferent[T](x: T, y: T, z: T) = x != y && x != z && y != z
If you call allDifferent(3, 4, 5)
, each integer is wrapped into a java.lang.Integer
before the method is called. Of course, one can manually supply an overloaded version
def allDifferent(x: Int, y: Int, z: Int) = ...
as well as seven more methods for the other primitive types.
You can generate these methods automatically by annotating the type parameter with @specialized
:
def allDifferent[@specialized T](x: T, y: T, z: T) = ...
You can restrict specialization to a subset of types:
def allDifferent[@specialized(Long, Double) T](x: T, y: T, z: T) = ...
In the annotation constructor, you can provide any subset of Unit
, Boolean
, Byte
, Short
, Char
, Int
, Long
, Float
, Double
.
If you mark a feature with the @deprecated
annotation, the compiler generates a warning whenever the feature is used. The annotation has two optional arguments, message
and since
.
@deprecated(message = "Use factorial(n: BigInt) instead")
def factorial(n: Int): Int = ...
The @deprecatedName
is applied to a parameter, and it specifies a former name for the parameter.
def draw(@deprecatedName('sz) size: Int, style: Int = NORMAL)
You can still call draw(sz = 12)
but you will get a deprecation warning.
Note
The constructor argument is a symbol—a name preceded by a single quote. Symbols with the same name are guaranteed to be unique. Therefore, comparing symbols is a bit more efficient than comparing strings. More importantly, there is a semantic distinction: A symbol denotes a name of some item in a program.
The @deprecatedInheritance
and @deprecatedOverriding
annotations generate warnings that inheriting from a class or overriding a method is now deprecated.
The @implicitNotFound
and @implicitAmbiguous
annotations generates meaningful error messages when an implicit value is not available or ambiguous. See Chapter 21 for details about implicits.
The @unchecked
annotation suppresses a warning that a match is not exhaustive. For example, suppose we know that a given list is never empty:
(lst: @unchecked) match {
case head :: tail => ...
}
The compiler won’t complain that there is no Nil
option. Of course, if lst
is Nil
, an exception is thrown at runtime.
The @uncheckedVariance
annotation suppresses a variance error message. For example, it would make sense for java.util.Comparator
to be contravariant. If Student
is a subtype of Person
, then a Comparator[Person]
can be used when a Comparator[Student]
is required. However, Java generics have no variance. We can fix this with the @uncheckedVariance
annotation:
trait Comparator[-T] extends
java.lang.Comparator[T @uncheckedVariance]
1. Write four JUnit test cases that use the @Test
annotation with and without each of its arguments. Run the tests with JUnit.
2. Make an example class that shows every possible position of an annotation. Use @deprecated
as your sample annotation.
3. Which annotations from the Scala library use one of the meta-annotations @param
, @field
, @getter
, @setter
, @beanGetter
, or @beanSetter
?
4. Write a Scala method sum
with variable integer arguments that returns the sum of its arguments. Call it from Java.
5. Write a Scala method that returns a string containing all lines of a file. Call it from Java.
6. Write a Scala object with a volatile Boolean field. Have one thread sleep for some time, then set the field to true
, print a message, and exit. Another thread will keep checking whether the field is true
. If so, it prints a message and exits. If not, it sleeps for a short time and tries again. What happens if the variable is not volatile?
7. Give an example to show that the tail recursion optimization is not valid when a method can be overridden.
8. Add the allDifferent
method to an object, compile and look at the bytecode. What methods did the @specialized
annotation generate?
9. The Range.foreach
method is annotated as @specialized(Unit)
. Why? Look at the bytecode by running
javap -classpath /path/to/scala/lib/scala-library.jar
scala.collection.immutable.Range
and consider the @specialized
annotations on Function1
. Click on the Function1.scala
link in Scaladoc to see them.
10. Add assert(n >= 0)
to a factorial
method. Compile with assertions enabled and verify that factorial(-1)
throws an exception. Compile without assertions. What happens? Use javap
to check what happened to the assertion call.