for registering the particular low-level operations.
Some parser could then look into the source code to find out which classes and functions are needed for the various operators.
Note
A framework is a collection of classes, interfaces, and singleton objects that provide a scaffolding structure to software. A framework is not an executable program itself, but a software project uses the framework to establish a standardized structure. Different projects using a particular framework thus exhibit a similar structure and if a developer knows one project embedded into a particular framework it will be easier to understand other projects using the same framework.
This method of letting classes announce themselves to a program frequently gets used in a server environment where the program needs to be able to communicate with clients over a network.
There is, however, a problem with this approach. Because the meta-information gets presented from inside the documentation, there is no possibility for the compiler to check the correctness of the tags. Concerning the compiler, the contents of the documentation are completely unimportant, and should be unimportant, because this is what the language specification says.
Annotations in Kotlin
Now the compiler is in a better situation. Because annotations are part of the language the compiler can check whether they exist, are spelled correctly, and have the correct parameters provided.
In the following sections we first discuss annotation characteristics, then annotations that Kotlin provides. We then cover how to build and use our own annotations.
Annotation Characteristics
@Target(...)
Here you specify the possible element types to which the annotation can be applied. The parameter is a comma-separated list of any of the following (all of them are fields of the enumeration kotlin.annotation.AnnotationTarget):CLASS: All classes, interfaces, singleton objects and annotation classes.
ANNOTATION_CLASS: Only annotation classes.
PROPERTY: Properties.
FIELD: A field that is the data holder for a property. Note that a property by virtue of getters and setters does not necessarily need a field. However, if there is a field, this annotation target points to that field. You put it in front of a property declaration, together with the PROPERTY target.
LOCAL_VARIABLE: Any local variable (val or var inside a function).
VALUE_PARAMETER: A function or constructor parameter.
CONSTRUCTOR: Primary or secondary constructor. If you want to annotate a primary constructor, you must use the notation with the constructor keyword added; for example, class Xyz @MyAnnot constructor(val p1:Int, ...).
FUNCTION: Functions (not including constructors).
PROPERTY_GETTER: Property getters.
PROPERTY_SETTER: Property setters.
TYPE: Annotations for types, as in val x: @MyAnnot Int = ...
EXPRESSION: Statements (must contain an expression).
FILE: File annotation. You must specify this before the package declaration and in addition add a file: between the @ and the annotation name, as in @file:AnnotationName.
TYPE_ALIAS: We didn’t talk about type aliases yet. They are just new names for types, as in typealias ABC = SomeClass<Int>. This annotation type is for such typealias declarations .
If unspecified, targets are CLASS, PROPERTY, LOCAL_VARIABLE, VALUE_PARAMETER, CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, and PROPERTY_SETTER.
@Retention(...)
This specifies where the annotation information goes during compilation and whether it is visible using one of the following (all are fields from the enumeration class kotlin.annotation.AnnotationRetention):SOURCE: Annotation exists only in the sources; the compiler removes it.
BINARY: Annotation exists in the compiled classes, interfaces, or singleton objects. It is not possible to query annotations at runtime using reflection.
RUNTIME: Annotation exists in the compiled classes, interfaces, or singleton objects, and it is not possible to query annotations at runtime using reflection.
The default is RUNTIME.
@Repeatable
Add this if you want to allow the annotation to appear more often than just once.
@MustBeDocumented
Add this if you want the annotation to show up in the public API documentation.
You can see that for classes, interfaces, singleton objects, properties, and local properties, you don’t have to specify special characteristics if you want the annotations to show up visibly in the compiled files.
Applying Annotations
file
We know that a Kotlin file can contain properties and functions outside classes, interfaces, and singleton objects. For an annotation applying to such a file, you write @file:AnnotationName in front of the package declaration. For example:@file:JvmName("Foo")package com.xyz.project...to give the internally created class the name Foo.
property
The annotation gets associated with the property. Note that if you use Java to access your Kotlin classes, this annotation is not visible to Java.
field
The annotation gets associated with the data field behind a property.
get
The annotation gets associated with the property getter.
set
The annotation gets associated with the property setter.
receiver
The annotation gets associated with the receiver parameter of an extension function or property.
param
The annotation gets associated with a constructor parameter.
setparam
The annotation gets associated with a property setter parameter.
delegate
The annotation gets associated with the field storing the delegate instance.
If you don’t specify a use-site target, the @Target meta-annotation is used to find the element to annotate. If there are several possibilities, the ranking is param > property > field.
Annotations with Array Parameter
Reading Annotations
For reading annotations with retention type SOURCE you need a special annotation processor. Remember that for SOURCE type annotation the Kotlin compiler removes the annotation during the compilation step, so in this case we must have some software looking at the sources before the compiler does its work. Most source type annotation processing happens inside bigger server framework projects; here the annotations get used to produce some synthetic Kotlin or Java code that glues together classes to model complex database structures. There is a special plug-in to be used for such purposes, KAPT, which allows for the inclusion of such source type annotation preprocessors.
You can find more information about KAPT usage in the online Kotlin documentation. For the rest of this section we talk about RUNTIME retention type annotation processing.
For reading annotations that have been compiled by the Kotlin compiler and ended up in the bytecode that gets executed by the runtime engine, the reflection API gets used. We discuss the reflection API later in this book; here we mention only annotation processing aspects.
Note
To use reflection, the kotlin-reflect.jar must be in the class path. This means you have to add implementation "org.jetbrains.kotlin: kotlin-reflect:$kotlin_version" inside the dependencies section of your module’s build.gradle file.
Annotations by Element
Element | Reading Annotations |
---|---|
Classes, singleton objects, and interfaces | Use TheName::class.annotations to get a list of kotlin.Annotation objects you can further investigate. You can, for example, use the property .annotationClass to get the class of each annotation. If you have a property and first need to get the corresponding class, use property::class.annotations To read a certain annotation, use val annot = TheName::class.findAnnotation<AnnotationType>() where for AnnotationType you substitute the annotation’s class name. From here you can, for example, read an annotation’s parameter via annot?.paramName. |
Properties | Use val prop = ClassName::propertyNameval annots = prop.annotationsval annot = prop.findAnnotation<AnnotationType>() to fetch a property by name and from there get a list of annotations or search for a certain annotation. |
Fields | To access a field’s annotations use val prop = ClassName::propertyNameval field = prop.javaFieldval annotations = field?.annotations |
Functions | To access a nonoverloaded function by name write TheClass::functionName. In case you have several functions using the same name but with different parameters you can write val funName = "functionName" // <- choose your ownval pars = listOf(Int::class) // <- choose your ownval function = TheClass::class. declaredFunctions.filter { it.name == funName } ?.find { f -> val types = f.valueParameters.map{ it.type.jvmErasure} types == pars } Once you have the function, you can use .annotations for a list of annotations, or .findAnnotation<AnnotationType>() to search for a certain annotation. |
Built-in Annotations
Built-in Annotations: General
Annotation Name | Package | Targets | Description |
---|---|---|---|
Deprecated | kotlin | class, annotation class, function, property, constructor, property setter, property getter, type alias | Takes three parameters: message:String, replaceWith:ReplaceWith = ReplaceWith("") and level:DeprecationLevel = DeprecationLevel.WARNING Mark the element as deprecated. DeprecationLevel is an enumeration with fields: WARNING, ERROR, HIDDEN |
ReplaceWith | kotlin | — | Takes two parameters: expression:String and vararg imports:String. Use this to specify a replacement code snippet inside @Deprecated. |
Suppress | kotlin | class, annotation class, function, property, field, local variable, value parameter, constructor, property setter, property getter, type, type alias, expression, file | Takes one vararg parameter: names:String. Retention type is SOURCE. Use this to suppress compiler warnings. The names parameter is a comma-separated list of warning message identifiers. Unfortunately finding an exhaustive list of compiler warning identifiers is not that easy, but Android Studio helps: Once a compiler warning appears, the corresponding construct gets highlighted and pressing Alt+Enter with the cursor over it allows us to generate a corresponding suppress annotation. See Figure 14-1 (use arrow keys to navigate in the menu). |
Custom Annotations
For the annotations for the annotation (i.e., the meta-annotations), note that they are all optional and the order is free. For their meanings, see the section “Annotation Characteristics” earlier in this chapter.
where the following parameter types are allowed: types that correspond to primitive types (i.e., Byte, Short, Int, Long, Char, Float, Double), strings, classes, enums, other annotations, and arrays of those. You can add vararg for a variable number of arguments. Note that for annotations used as parameters for other annotations, the @ for the parameter annotations gets omitted.
It finds the function corresponding to the first parameter. The Calculator::class.declaredFunctions lists all the directly declared functions of the Calculator class. This means it does not also look into superclasses. The find selects divide() or multiply().
From the function, we loop though the parameters via .valueParameters. For each parameter, we see whether it has annotation NotZero associated with it. If it does, we check the actual parameter, and if it is 0.0, we throw an exception.
If no exception was thrown, we invoke the function. The arrayOf() expression concatenates the receiver object and the function parameters into a single Array<Any>.
To see whether the annotation works, try another invocation with 0.0 as the second parameter.
Exercise 1
To the Calculator example, add a new annotation @NotNegative and a new operation sqrt() for the square root. Make sure a negative parameter for this operator is not allowed. Note: The actual square root gets calculated via java.lang.Math.sqrt().