Chapter 15. Annotations

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.

15.1 What Are Annotations?

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.

15.2 What Can Be Annotated?

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.

15.3 Annotation Arguments

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.

15.4 Annotation Implementations

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


Image 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.

15.5 Annotations for Java Features

The Scala library provides annotations for interoperating with Java. They are presented in the following sections.

15.5.1 Java Modifiers

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]

15.5.2 Marker Interfaces

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


Image Note

For more information about Java concepts such as volatile fields, cloning, or serialization, see C. Horstmann, Core Java®, Tenth Edition (Prentice Hall, 2016).


15.5.3 Checked Exceptions

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.

15.5.4 Variable Arguments

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.

15.5.5 JavaBeans

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.


Image 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.


15.6 Annotations for Optimizations

Several annotations in the Scala library let you control compiler optimizations. They are discussed in the following sections.

15.6.1 Tail Recursion

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.


Image 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


15.6.2 Jump Table Generation and Inlining

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.

15.6.3 Eliding Methods

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.


Image Note

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.


Image 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.


15.6.4 Specialization for Primitive Types

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.

15.7 Annotations for Errors and Warnings

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.


Image 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]

Exercises

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset