1. Overview
In this tutorial, we’ll give an overview of Kotlin annotations.
We’ll demonstrate how to apply them, how to create and customize our own ones. We’ll then briefly discuss the interplay between annotations and keywords in Java and Kotlin.
Finally, we’ll give a simple example of a class validation that illustrates how we can process the annotations.
2. Applying Annotations
In Kotlin, we apply an annotation by putting its name prefixed with the @ symbol in front of a code element. For example, if we want to apply an annotation named Positive, we should write the following:
@Positive val amount: Float
Very often, annotations have parameters. The annotation parameters must be compile-time constants and must be of the following types:
- Kotlin primitive types (Int, Byte, Short, Float, Double, Char, Boolean)
- enumerations
- class references
- annotations
- arrays of the above-mentioned types
If an annotation requires a parameter, we provide its value in parenthesis like in a function call:
@SinceKotlin(version="1.3")
In the case, when an annotation parameter is an annotation too, then we should omit the @ symbol*:*
@Deprecated(message="Use rem(other) instead", replaceWith=ReplaceWith("rem(other)"))
If an annotation parameter is a class object, we should add ::class to the class name, for example:
@Throws(IOException::class)
If we need to specify that an annotation parameter may have multiple values, then we just pass an array of those values:
@Throws(exceptionClasses=arrayOf(IOException::class, IllegalArgumentException::class))
Starting from Kotlin 1.2, we may use the following syntax as well:
@Throws(exceptionClasses=[IOException::class, IllegalArgumentException::class])
3. Declaring Annotations
In order to declare an annotation, we define a class and place the annotation keyword before the class one. By their nature, declarations of annotation cannot contain any code.
The simplest annotation has no parameters:
annotation class Positive
Declaration of an annotation that requires a parameter is like a class with a primary constructor:
annotation class Prefix(val prefix: String)
When we declare our custom annotations, we should specify to which code elements they might apply and where they should be stored. Annotations used to define this meta-information are called meta-annotations.
In the next sections, let’s briefly discuss them. For the most recent information, we can always check the official documentation.
3.1. @Target
This meta-annotation specifies which code elements this annotation may refer to. It has a required parameter that must be an instance of the AnnotationTarget enumeration or an array thereof. Therefore, we may specify that we want to apply our annotation to the following elements:
- CLASS
- ANNOTATION_CLASS
- TYPE_PARAMETER
- PROPERTY
- FIELD
- LOCAL_VARIABLE
- VALUE_PARAMETER
- CONSTRUCTOR
- FUNCTION
- PROPERTY_GETTER
- PROPERTY_SETTER
- TYPE
- EXPRESSION
- FILE
- TYPEALIAS
If we do not specify it explicitly, the corresponding annotation can be applied to the following elements by default:
CLASS, PROPERTY, FIELD, LOCAL_VARIABLE, VALUE_PARAMETER, CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER
3.2. @Retention
This meta-annotation specifies whether the annotation should be stored in the .class file and whether it should be visible for a reflection. Its required parameter must be an instance of the AnnotationRetention enumeration that has the following elements:
- SOURCE
- BINARY
- RUNTIME
Unlike in Java, the default value for the @Retention in Kotlin is RUNTIME.
3.3. @Repeatable
@Repeatable specifies whether an element might have multiple annotations of the same type. This meta-annotation accepts no parameters.
3.4. @MustBeDocumented
@MustBeDocumented specifies whether the annotation’s documentation should be included in the generated documentation. This meta-annotation accepts no parameters as well.
3.5. Nested Declarations in Annotations
Starting Kotlin 1.3, we can declare annotations within other annotations. Additionally, we can also declare enumerations or companion within an annotation.
Let’s understand this by declaring the Child1 and Child2 annotations within the Parent annotation:
annotation class Parent(val type: Type) {
annotation class Child1(val prop1: String)
annotation class Child2(val prop2: Int)
enum class Type { TYPE1, TYPE2 }
}
We must note that we also declared a Type enum within the Parent annotation and added that as part of the primary constructor.
Now, let’s apply this annotation with the ClassUsingNestedAnnotation class:
@Parent(Parent.Type.TYPE1)
@Parent.Child1(prop1 = "sample prop")
@Parent.Child2(prop2 = 1)
class ClassUsingNestedAnnotation {
}
It’s important to note that we’ve defined the type, prop1, and prop2 fields while applying the annotation.
Next, let’s verify that Child1 annotation is applied correctly:
val child1Annotation = ClassUsingNestedAnnotation::class.findAnnotation<Parent.Child1>()
assertEquals("sample prop", child1Annotation?.prop1)
It’s worth noting that we retrieved the nested annotation with the kotlin.reflect.full.findAnnotation() method.
Similarly, we can check that the Child2 annotation is correctly attached to the ClassUsingNestedAnnotation class:
val child2Annotation = ClassUsingNestedAnnotation::class.findAnnotation<Parent.Child2>()
assertEquals(1, child2Annotation?.prop2)
Lastly, let’s also confirm that the type enumeration is attached correctly:
val parentAnnotation = ClassUsingNestedAnnotation::class.findAnnotation<Parent>()
assertEquals(Parent.Type.TYPE1, parentAnnotation?.type)
Great! Everything worked as expected.
4. Java-Interoperability With Annotations
Kotlin is usually more concise with respect to Java. For example, it automatically creates additional methods for us, like when we declare a property:
val name: String?;
the compiler automatically creates a private field and a getter and setter for this property. As a result, a question arises: if we add an annotation to property, where it will be applied? To the getter, setter or to the field itself?
In Kotlin, if we decorate a property with an annotation defined in a Java code, it gets applied to the corresponding field.
Now we may face a problem if the annotation requires a field to be public, for example, with JUnit’s @Rule annotation. In order to avoid ambiguity, Kotlin has the so-called use-site target declaration.
4.1. Use-Site Target Declarations
The use-site targets are optional. We place them between the @ symbol and the annotation name, using the colon sign as a separator. The syntax allows us to specify multiple annotation names at once:
In the case of placing @get:Positive on a Kotlin field, it would mean that the annotation should actually target the generated getter for that field.
Kotlin supports the following values of the use-site targets that correspond to:
- delegate – a field storing a delegated property
- field – a field generated for a property
- file – a class containing top-level functions and properties defined in that file
- get/set – the property getter/setter
- param – a constructor parameter
- property – the Kotlin’s property, it is not accessible from Java code
- receiver – the receiver parameter of an extension function or property
4.2. JVM-Related Annotations
The following Kotlin annotations allow us to customize how they can be used from the Java code:
- @JvmName – permits to change the name of the generated Java method or field
- @JvmStatic – allows us to specify that the generated Java method or field should be static
- @JvmOverloads – indicates that the Kotlin compiler should generate overload functions substituting default parameters
- @JvmField – indicates that generated Java field should be a public one with no getter/setter
Some Java annotations become Kotlin’s keywords and vice-versa:
Java
Kotlin
@Override
override
volatile
@Volatile
strictfp
@Strictfp
synchronized
@synchronized
transient
@Transient
throws
@Throws
5. Processing Annotations
In order to demonstrate how we can process annotations, let’s create a simple validator. Here we present only the idea while the complete code is available in our repository on Github.
Suppose that we should decide whether an instance of an Item is valid:
class Item(val amount: Float, val name: String)
We assume that an Item instance is valid if the value of the amount is positive and the value of the name is either Alice or Bob.
To this end, let’s decorate the properties of the Item class with our custom annotations: Positive and AllowedNames:
class Item(
@Positive val amount: Float,
@AllowedNames(["Alice", "Bob"]) val name: String)
In our naive implementation, we just get the Item‘s properties:
val fields = item::class.java.declaredFields
for (field in fields) {...}
and iterate over all annotations of each property:
for (annotation in field.annotations) {...}
in order to find the ones with which we have decorated Item‘s properties.
For example, we may detect whether the AllowedNames is present on a property by means of the command:
field.isAnnotationPresent(AllowedNames::class.java)
Once the annotation is present, we may easily decide whether the property has a valid value just by comparing it with the allowed values:
val allowedNames = field.getAnnotation(AllowedNames::class.java)?.names
We should be aware that annotations use the Java Reflection API. As a result, the performance of the code might suffer if we rely heavily on annotations.
6. Conclusion
In this article, we’ve considered Kotlin annotations and their Java counterparts. We’ve described how to apply Kotlin annotations, how to create custom ones, and then how to process them.
As we can see, Kotlin annotations are quite similar to those in Java. Nevertheless, our tutorial Creating a Custom Annotation in Java may also prove useful.
As always, the complete code is available over in our GitHub repository.