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.
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.
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)
//
Methoddef display(@nowarn message: String) = "" //
Parametercase 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.
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.
The Scala library provides annotations for interoperating with Java. They are presented in the following sections.
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:
name: String
name_=(newValue: String): Unit
getName(): String
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.
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)
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.
Note
For more information about Java concepts such as volatile fields or serialization, see C. Horstmann, Core Java, Twelfth Edition (Prentice Hall, 2022).
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.
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.
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]
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 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
.
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
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("
")
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
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.
Tip
If you use the compiler flag -Wunused:nowarn
, the compiler checks that each @nowarn
annotation actually suppresses a warning message.
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
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.
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?