You already know about annotations – a way of providing metadata about our code – and how to apply them. Today, we will learn about custom annotations, which is useful for two reasons. First, we'll get a deeper understanding of built-in annotations and of how to use them properly. Second, we will learn to do the magic by ourselves and make our own custom annotations, which you can use, for example, when you create your own library or framework. Now let's get started!
Your first custom annotation
Declaring an annotation is similar to declaring a class – just add the annotation modifier in front of it:
annotation class CustomSuppress
Annotations can also have constructors that take parameters:
annotation class CustomSuppress(vararg val errorName: String)
Notice that vararg means that we can pass zero or more arguments, which is ideal in this situation.
Now let's use our previous custom annotation. Consider the example below:
fun main() {
@CustomSuppress("UNUSED_VARIABLE")
val userName = "Alex"
// This is also correct
// because vararg means zero or more arguments
@CustomSuppress
val phone = 1234567
}
Yes, it works and there are no errors! Now, if someone writes this code in the IDE, they may wonder why our @CustomSuppress didn't suppress anything. Where is the magic?! Actually, to understand how annotations do their magic, we need to talk about "Annotation processing", which is out of scope of this topic, so now we'll just focus on creating annotations and applying them.
Parameters
What types of parameters are allowed to be passed to an annotation's constructor?
According to the official documentation, the allowed parameter types include:
Types that correspond to Java primitive types (Int, Long, etc.)
Strings
Classes (
Foo::class)Enums
Other annotations
Arrays of the types listed above
The question now is: what is not allowed? Again quoting the official documentation, "Annotation parameters cannot have nullable types because the JVM does not support storing null as a value of an annotation attribute". The code in the following example will give an error:
// Compile time error
// nullable types are NOT allowed
annotation class CustomSuppress(vararg val errorName: String?)
Last but not least, if you pass an annotation as a parameter of another annotation, don't prefix it with the @ character and do it like in the example below:
// A new custom annotation
annotation class Special(val why: String)
// Passing Special annotation as parameter
annotation class CustomSuppress(val name: Special)
We have created two custom annotations and passed the Special annotation as a parameter to the second one – CustomSuppress. Simply put, think about passing it as a parameter like in the case of normal class instantiation.
Now, let's use CustomSuppress from the example above and apply it:
fun main() {
@CustomSuppress(name = Special("IDK"))
val userName = "Alexandr"
}
To avoid confusion, applying an annotation is completely different from passing it as a parameter: to apply an annotation, you need to use the @ symbol. So let's edit the previous example:
// A new Custom annotation
annotation class Special(val why: String)
// Passing Special annotation as parameter
// Applying @Special to this parameter
annotation class CustomSuppress(@Special("IDK") val name: Special)
The last thing to be mentioned in this section is that an annotation parameter cannot be 'var':
//Compile time error
annotation class CustomSuppress(var name: String)Meta-annotations
Annotations provide metadata about our code, right? However, we can also use annotations to provide metadata about other annotations. The annotations that annotate other annotations are called "Meta-annotations".
Meta-annotations are the following:
1. @Target: as the name implies, this annotation indicates which kinds of elements can be targeted. Consider the example below:
@Target(AnnotationTarget.LOCAL_VARIABLE)
annotation class Special
AnnotationTarget is an enum that contains a list of possible targets; you can find the full list in the documentation.
The @Special annotation is applicable only to local variables. Consider the example below:
fun main() {
// applicable to local variables
@Special val name = "Ali"
// Error as this annotation
// is not applicable to target 'expression'
println(@Special name)
}
You can also target as many elements as you want:
@Target(
AnnotationTarget.LOCAL_VARIABLE,
AnnotationTarget.FIELD,
AnnotationTarget.FUNCTION,
AnnotationTarget.CLASS
)
annotation class Special
If the @Target meta-annotation is not present in the annotation declaration, the annotation is applicable to the following elements by default:
CLASS
PROPERTY
FIELD
LOCAL_VARIABLE
VALUE_PARAMETER
CONSTRUCTOR
FUNCTION
PROPERTY_GETTER
PROPERTY_SETTER.
The @Target meta-annotation is very useful when you need to make your custom annotation work only with specific elements: it guarantees that you always get the expected results.
2. @Retention: this meta-annotation determines whether the annotation will be stored in binary output or the compiler will get rid of it when the program runs.
There are only three available options in the enum AnnotationRetention that we can choose from:
SOURCE | Annotation isn't stored in binary output. |
BINARY | Annotation is stored in binary output but is invisible for reflection. |
RUNTIME | Annotation is stored in binary output and is visible for reflection (default retention). |
Let's take an example:
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
annotation class VeryCool
We have created an annotation called VeryCool, chosen functions as the only target, and finally specified that the annotation's retention will be SOURCE, which means that the annotation won't be stored in binary output and will be deleted when the program runs.
You may ask: why do we create an annotation that will be deleted later? Sometimes it's useful when we need the annotation to work only at the compile time, like in the case with the @Suppress annotation.
3. Sometimes you want to apply an annotation to the same element multiple times, and here @Repeatable comes to the rescue:
fun main() {
@Special
@Special
@Special
val name = "Ali"
}
@Repeatable
annotation class Special
4. @MustBeDocumented specifies that the annotation must be part of the element's generated documentation.
Conclusion
In this topic, we have learned how to make our own custom annotations and how to specify their parameters. We've also learned different meta-annotations like @Target, @MustBeDocumented, @Repeatable, and @Retention.
Annotating is easy, even if it might be a bit tricky the first time. Now, let's enforce our understanding with some practice.