Chapter 15. Annotations

Topics in This Chapter A2

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, ...).

  • You can use Java annotations in Scala code. They are retained in the class files.

  • @volatile, @transient, and @native generate the equivalent Java modifiers.

  • Use @throws to generate Java-compatible throws specifications.

  • Use the @BeanProperty annotation to generate the JavaBeans getXxx/setXxx methods.

  • The @tailrec annotation lets you verify that a recursive function uses tail call optimization.

  • 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 and enterprise technologies such as Jakarta EE.

Annotations start with an @. For example:

case class Person @JsonbCreator (
  @JsonbProperty val name: String,
  @JsonbProperty val age: Int)

You can use Java annotations with Scala classes. The annotations in the preceding example are from JSON-B, a Java framework for converting between JSON and Java classes. The framework has no knowledge of Scala. We will be using JSON-B for several examples, but you don’t have to be familiar with its details. If you are curious, read through the tutorial at https://javaee.github.io/jsonb-spec/users-guide.html.

Scala provides its own annotations that are processed by the Scala compiler or a compiler plugin. (Implementing a compiler plugin is a nontrivial undertaking that is not covered in this book.)

You have seen the Scala @main annotation throughout the book for marking a program’s entry point.

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 the example above, the constructor and its parameters are annotated in the class file.

In Scala, annotations can affect the compilation process. For example, the @main annotation causes the generation of a Java class with a public static void main(String[] args) method.

15.2 Annotation Placement

In Scala, you can place annotations before classes, methods, fields, local variables, parameters, and type parameters:

@deprecated class Sample : // Class
  @volatile var alive = true // Field
  @tailrec final def gcd(a: Int, b: Int): Int = if b == 0 then a else gcd(b, a % b)
    // Method
  def display(@nowarn message: String) = "" // Parameter
case class Box[@specialized T](value: T) // Type parameter

You can apply multiple annotations. The order doesn’t matter.

@BeanProperty @JsonbProperty val age: Int

When annotating the primary constructor, place the annotation after the class name:

class Person @JsonbCreator (...)

You can also annotate expressions. Add a colon followed by the annotation, for example:

(props.get(key): @unchecked) match { ... }
  // The expression props.get(key) is annotated

Annotations on a type are placed after the type, like this:

val country: String @Localized = java.util.Locale.getDefault().getDisplayCountry()

Here, the String type is annotated. The method returns a localized string.

15.3 Annotation Arguments

Annotations can have named arguments, such as

@JsonbProperty(value="p_name", nillable=true) var name: String = null

Most annotation arguments have defaults. For example, the nillable argument of the @JsonbProperty annotation has a default value of false, and the value attribute has a default of "".

When you provide only the argument named value, then value= is optional. For example:

@JsonbProperty("p_age") var age : Int = 0
  // The value argument is "p_age"

If the annotation has no arguments, the parentheses can be omitted:

@JsonbTransient val nice: Boolean = true

Arguments of Java annotations are restricted to the following types:

  • Numeric or Boolean 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.

15.4 Annotations for Java Features

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

15.4.1 Bean Properties

When you declare a public field in a class, Scala provides a getter and (for a var) a setter method. However, the names of these methods are not what Java tools expect. The JavaBeans specification (www.oracle.com/technetwork/articles/javaee/spec-136004.html) defines a Java property as a pair of getFoo/setFoo methods (or just a getFoo method for a read-only property). Many Java tools rely on this naming convention.

When you annotate a Scala field with @BeanProperty, then such methods are automatically generated. For example,

import scala.beans.BeanProperty

class Person :
  @BeanProperty var name = ""

generates four methods:

  1. name: String

  2. name_=(newValue: String): Unit

  3. getName(): String

  4. setName(newValue: String): Unit

However, the getName and setName methods are only for the benefit of Java. The Scala compiler will refuse to invoke them. In Scala, read or write the name property.

The @BooleanBeanProperty annotation generates a getter with an is prefix for a Boolean method.

Images Note

If you define a field as a primary constructor parameter, and you want JavaBeans getters and setters, annotate the constructor parameter like this:

class Person(@BeanProperty var name: String)

15.4.2 Serialization

With serializable classes, you can use the @SerialVersionUID annotation to specify the serial version:

@SerialVersionUID(6157032470129070425L)
class Employee extends Person, Serializable :

The @transient annotation marks a field as transient:

@transient var lastLogin: ZonedDateTime = null
  // Becomes a transient field in the JVM

A transient field is not serialized.

Images Note

For more information about Java concepts such as volatile fields or serialization, see C. Horstmann, Core Java, Twelfth Edition (Prentice Hall, 2022).

15.4.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,

@throws(classOf[IOException]) def save(filename: String) = ...

The Java signature is

void save(String filename) throws IOException

Without the @throws annotation, the Java code would not be able to catch the exception.

try { // This is Java
  fred.save("/etc/fred.ser");
} catch (IOException ex) {
  System.out.println("Error saving: " + ex.getMessage());
}

The Java compiler needs to know that the save method can throw an IOException, or it will refuse to catch it.

15.4.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 turns the variable argument into a sequence parameter:

def process(args: Seq[String])

That method would be very cumbersome to call 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.4.5 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 @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 Annotations for Optimizations

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

15.5.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 then BigInt(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 then partial else sum2(xs.tail, xs.head + partial)

The partial sum is passed as an argument; 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

Util.sum(1 to 1000000)

you will get a stack overflow error (at least with the default stack size of the JVM), but

Util.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 method is in a class instead of an object:

class Util :
  @tailrec def sum3(xs: Seq[Int], partial: BigInt): BigInt =
    if xs.isEmpty then partial else sum3(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.

Images 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(expr) where expr is the next expression to be evaluatued. The expression returns a TailRec[A]. Here is a simple example:

import scala.util.control.TailCalls.*
def evenLength(xs: Seq[Int]): TailRec[Boolean] =
  if xs.isEmpty then done(true) else tailcall(oddLength(xs.tail))
def oddLength(xs: Seq[Int]): TailRec[Boolean] =
  if xs.isEmpty then 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.5.2 Lazy Values

A lazy value is initialized when it is first accessed:

lazy val words =
  scala.io.Source.fromFile("/usr/share/dict/words").mkString.split("
")

If you never use words, the file is not read in at all. If you use it multiple times, the file is only read with the first use.

Since it is possible for that first use to occur concurrently in multiple threads, each access of a lazy val invokes a method that acquires a lock.

If you know that you never have such a concurrent access, you can avoid the locking with the @threadUnsafe annotation:

@threadUnsafe lazy val words =
  scala.io.Source.fromFile("/usr/share/dict/words").mkString.split("
")

15.6 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 display(message: String, @deprecatedName("sz") size: Int,
  font: String = "Sans") = ...

You can still call draw(sz = 12) but you will get a deprecation warning.

The @deprecatedInheritance and @deprecatedOverriding annotations generate warnings that inheriting from a class or overriding a method is now deprecated.

Some Scala features are deemed experimental. To access them, you need to enter “experimental scope” with an @experimental annotation:

@experimental @newMain def main(name: String, age: Int) =
  println(s"Hello $name, next year you’ll be ${age + 1}")

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 case. 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.util.Comparator[T @uncheckedVariance]

Finally, you can selectively hide warnings with the @nowarn annotation. For example, the stop method of the Java Thread class is deprecated. When you call

myThread.stop()

the compiler generates a deprecation warning. You can turn it off like this:

myThread.stop() : @nowarn

or

myThread.stop() : @nowarn("cat=deprecation") // Silences the deprecation category

Images Note

The optional argument of the @nowarn annotation can be any valid filter for the -Wconf compiler flag. Run the Scala command-line compiler with the -Wconf:help flag to get a summary of the filter syntax.

Images Tip

If you use the compiler flag -Wunused:nowarn, the compiler checks that each @nowarn annotation actually suppresses a warning message.

15.7 Annotation Declarations

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 declarations of the existing annotation classes.

An annotation must extend the Annotation trait. For example, the unchecked annotation is defined as follows:

final class unchecked extends scala.annotation.Annotation

An annotation extending StaticAnnotation persists in class files:

class deprecatedName(name: String, since: String) extends StaticAnnotation

A ConstantAnnotation can only be constructed with numbers, Boolean values, strings, enumerations, class literals, and arrays thereof. Here is an example:

class SerialVersionUID(value: Long) extends ConstantAnnotation

Images CAUTION

The annotations that Scala places in class files are in a different format than Java annotations and cannot be read by the Java virtual machine. If you want to implement a new Java annotation, you need to write the annotation class in Java.

Generally, an annotation belongs only to the expression, variable, field, method, class, or type to which it is applied. For example, the annotation

def display(@nowarn message: String) = ""

applies only to one element: the parameter variable message.

However, field definitions in Scala can give rise to multiple features in Java, all of which can potentially be annotated. For example, consider

class Person(@JsonbProperty @BeanProperty var name: String)

Here, there are six items that can be targets for the @JsonbProperty annotation:

  • The constructor parameter

  • The private instance field

  • The accessor method name

  • The mutator method name_=

  • The bean accessor getName

  • The bean mutator setName

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 ConstantAnnotation

You can also apply these annotations in an ad-hoc fashion:

@(JsonbProperty @beanGetter @beanSetter) @BeanProperty var name: String = null

In this situation, the @JsonbProperty annotation is applied to the Java getName method.

Exercises

1. Using the Java JUnit library, write a class with test cases in Scala. Use the @Test annotation and a couple of other annotations of your choice.

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. Make a class Student with read-write JavaBeans properties name (of type String) and id (of type Long). What methods are generated? (Use javap to check.) Can you call the JavaBeans getters and setters in Scala? Should you?

8. Consider these recursive functions for repeatedly applying a function to an initial value until a fixed point is found (that is, a value x such that f(x) == x).

def fix(f: Double => Double)(x: Double): Double =
  val y = f(x)
  if x == y then x
  else fix(f)(y)

def fixpath(f: Double => Double)(x: Double): List[Double] =
  val y = f(x)
  if x == y then List()
  else y :: fixpath(f)(y)

The first function yields the fixed point, the second the sequence of all intermediate values. For example, try out

fix(Math.cos)(0)
fixpath(Math.cos)(0)

Which is tail-recursive? When not, can you provide an implementation that is?

9. Give an example to show that the tail recursion optimization is not valid when a method can be overridden.

10. Try finding an experimental feature in your Scala release and use it with the @experimental annotation.

11. In Scala 3.2, the @newMain experimental feature substantially improved on the command line parsing of its @main predecessor. Perhaps it is no longer experimental by the time you read this, and hopefully it has a nicer name. Use it to write a grep-like application that accepts on the command line a regular expression to search for, a file name, and flags for case sensitivity and inverting the match (that is, only printing nonmatching lines).

12. Experiment with the @nowarn annotation and the filter syntax. Write some code that produces warnings and then turn them off with @nowarn and appropriate filters. What happens if you add @nowarn to an expression that doesn’t produce a warning and use the -Wunused:nowarn flag? Can you turn that warning off with another @nowarn?

Can you apply @nowarn to something other than expressions?

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

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