Annotations are structured information added to program source code. Like comments, they can be sprinkled throughout a program and attached to any variable, method, expression, or other program element. Unlike comments, they have structure, thus making them easier to machine process.
This chapter shows how to use annotations in Scala. It shows their general syntax and how to use several standard annotations.
This chapter does not show how to write new annotation processing tools, because it is beyond the scope of this book. Chapter 29 shows one technique, but not the only one. Instead, this chapter focuses on how to use annotations, because it is more common to use annotations than to define new annotation processors.
There are many things you can do with a program other than compiling and running it. Some examples are:
Such tools are called meta-programming tools, because they are programs that take other programs as input. Annotations support these tools by letting the programmer sprinkle directives to the tool throughout their source code. Such directives let the tools be more effective than if they could have no user input. For example, annotations can improve the previously listed tools as follows:
In all of these cases, it would in theory be possible for the programming language to provide ways to insert the extra information. In fact, most of these are directly supported in some language or another. However, there are too many such tools for one language to directly support them all. Further, all of this information is completely ignored by the compiler, which after all just wants to make the code run.
Scala's philosophy in cases like this is to include the minimum, orthogonal support in the core language such that a wide variety of meta-programming tools can be written. In this case, that minimum support is a system of annotations. The compiler understands just one feature, annotations, but it doesn't attach any meaning to individual annotations. Each meta-programming tool can then define and use its own specific annotations.
A typical use of an annotation looks like this:
@deprecated def bigMistake() = //...The annotation is the @deprecated part, and it applies to the entirety of the bigMistake method (not shown—it's too embarrassing). In this case, the method is being marked as something the author of bigMistake wishes you not to use. Maybe bigMistake will be removed entirely from a future version of the code.
In the previous example, a method is annotated as @deprecated. Annotations are allowed in other places too. Annotations are allowed on any kind of declaration or definition, including vals, vars, defs, classes, objects, traits, and types. The annotation applies to the entirety of the declaration or definition that follows it:
@deprecated class QuickAndDirty { //... }Annotations can also be applied to an expression, as with the @unchecked annotation for pattern matching (see Chapter 15). To do so, place a colon (:) after the expression and then write the annotation. Syntactically, it looks like the annotation is being used as a type:
(e: @unchecked) match { // non-exhaustive cases... }Finally, annotations can be placed on types. Annotated types are described later in this chapter.
So far the annotations shown have been simply an at sign followed by an annotation class. Such simple annotations are common and useful, but annotations have a richer general form:
The annot specifies the class of annotation. All annotations must include that much. The exp parts are arguments to the annotation. For annotations like @deprecated that do not need any arguments, you would normally leave off the parentheses, but you can write @deprecated() if you like. For annotations that do have arguments, place the arguments in parentheses, for example, @serial(1234).
The precise form of the arguments you may give to an annotation depends on the particular annotation class. Most annotation processors only let you supply immediate constants such as 123 or "hello". The compiler itself supports arbitrary expressions, however, so long as they type check. Some annotation classes can make use of this, for example, to let you refer to other variables that are in scope:
@cool val normal = "Hello" @coolerThan(normal) val fonzy = "Heeyyy"
The name=const pairs in the general syntax are available for more complicated annotations that have optional arguments. These arguments are optional, and they can be specified in any order. To keep things simple, the part to the right-hand side of the equals sign must be a constant.
Scala includes several standard annotations. They are for features that are used widely enough to merit putting in the language specification, but that are not fundamental enough to merit their own syntax. Over time, there should be a trickle of new annotations that are added to the standard in just the same way.
Sometimes you write a class or method that you later wish you had not. Once it is available, though, code written by other people might call the method. Thus, you cannot simply delete the method, because this would cause other people's code to stop compiling.
Deprecation lets you gracefully remove a method or class that turns out to be a mistake. You mark the method or class as deprecated, and then anyone who calls that method or class will get a deprecation warning. They had better heed this warning and update their code! The idea is that after a suitable amount of time has passed, you feel safe in assuming that all reasonable clients will have stopped accessing the deprecated class or method and thus that you can safely remove it.
You mark a method as deprecated simply by writing @deprecated before it. For example:
@deprecated def bigMistake() = //...Such an annotation will cause the Scala compiler to emit deprecation warnings whenever Scala code accesses the method.
Concurrent programming does not mix well with shared mutable state. For this reason, the focus of Scala's concurrency support is message passing and a minimum of shared mutable state. See Chapter 30 for the details.
Nonetheless, sometimes programmers want to use mutable state in their concurrent programs. The @volatile annotation helps in such cases. It informs the compiler that the variable in question will be used by multiple threads. Such variables are implemented so that reads and writes to the variable are slower, but accesses from multiple threads behave more predictably.
The @volatile keyword gives different guarantees on different platforms. On the Java platform, however, you get the same behavior as if you wrote the field in Java code and marked it with the Java volatile modifier.
Many languages include a framework for binary serialization. A serialization framework helps you convert objects into a stream of bytes and vice versa. This is useful if you want to save objects to disk or send them over the network. XML can help with the same goals (see Chapter 26), but it has different trade offs regarding speed, space usage, flexibility, and portability.
Scala does not have its own serialization framework. Instead, you should use a framework from your underlying platform. What Scala does is provide three annotations that are useful for a variety of frameworks. Also, the Scala compiler for the Java platform interprets these annotations in the Java way (see Chapter 29).
The first annotation indicates whether a class is serializable at all. Most classes are serializable, but not all. A handle to a socket or GUI window, for example, cannot be serialized. By default, a class is not considered serializable. You should add a @serializable annotation to any class you would like to be serializable.
The second annotation helps deal with serializable classes changing as time goes by. You can attach a serial number to the current version of a class by adding an annotation like @SerialVersionUID(1234), where 1234 should be replaced by your serial number of choice. The framework should store this number in the generated byte stream. When you later reload that byte stream and try to convert it to an object, the framework can check that the current version of the class has the same version number as the version in the byte stream. If you want to make a serialization-incompatible change to your class, then you can change the version number. The framework will then automatically refuse to load old instances of the class.
Finally, Scala provides a @transient annotation for fields that should not be serialized at all. If you mark a field as @transient, then the framework should not save the field even when the surrounding object is serialized. When the object is loaded, the field will be restored to the default value for the type of the field annotated as @transient.
Scala code normally does not need explicit get and set methods for fields, because Scala blends the syntax for field access and method invocation. Some platform-specific frameworks do expect get and set methods, however. For that purpose, Scala provides the @scala.reflect.BeanProperty annotation. If you add this annotation to a field, the compiler will automatically generate get and set methods for you. If you annotate a field named crazy, the get method will be named getCrazy and the set method will be named setCrazy.
The generated get and set methods are only available after a compilation pass completes. Thus, you cannot call these get and set methods from code you compile at the same time as the annotated fields. This should not be a problem in practice, because in Scala code you can access the fields directly. This feature is intended to support frameworks that expect regular get and set methods, and typically you do not compile the framework and the code that uses it at the same time.
The @unchecked annotation is interpreted by the compiler during pattern matches. It tells the compiler not to worry if the match expression seems to leave out some cases. See Section 15.5 for details.
This chapter described the platform-independent aspects of annotations that you will most commonly need to know about. First of all it covered the syntax of annotations, because using annotations is far more common than defining new ones. Second it showed how to use several annotations that are supported by the standard Scala compiler, including @deprecated, @volatile, @serializable, @BeanProperty, and @unchecked.
Chapter 29 gives additional, Java-specific information on annotations. It covers annotations only available when targeting Java, additional meanings of standard annotations when targeting Java, how to interoperate with Java-based annotations, and how to use Java-based mechanisms to define and process annotations in Scala.