Annotations are a powerful feature in the Kotlin programming language that allows you to add metadata and additional information to your code. They provide a way to decorate classes, functions, properties, and other program elements with custom markers, which can be processed at compile time or runtime. In this blog post, we will dive deep into Kotlin annotations, exploring their syntax, usage, and various aspects, along with practical examples to illustrate their capabilities.
Kotlin Annotations Basics
In programming, metadata refers to additional information about a piece of code or data. It provides context, instructions, or descriptive details that can be used by tools, frameworks, or other parts of the system.
Annotations in Kotlin are a way to attach metadata to declarations in your code. They allow you to add information or instructions to classes, functions, properties, or other elements. Annotations are defined using special syntax and are prefixed with the @
symbol.
Annotations can have parameters that allow you to provide specific values or arguments. These parameters can be of different types, such as strings, numbers, classes, or even other annotations.
When you apply an annotation to a declaration, you associate that metadata with the declaration. The metadata can then be processed or accessed by various tools, frameworks, or libraries during compilation, runtime, or reflection.
Let’s look at a simple example to understand annotations better:
// Define a custom annotation
annotation class MyAnnotation(val message: String)
// Apply the annotation to a function
@MyAnnotation("This is my function")
fun myFunction() {
// Function implementation
}
In this example, we define a custom annotation(don’t worry! We discuss it later) called MyAnnotation
with a parameter message
. We then apply this annotation to the myFunction
function.
The annotation @MyAnnotation("This is my function")
serves as metadata attached to the function declaration. It provides additional information or instructions about the function.
The metadata provided by the annotation can be used by various tools or frameworks. For instance, a documentation generation tool may use annotation to include the message in the generated documentation. A code analyzer or linter may use the annotation to perform specific checks or enforce coding standards related to the function.
Annotations can also be processed at runtime using reflection. Reflection allows you to inspect and manipulate code and data during program execution. You can use reflection to access annotations attached to declarations and perform actions based on the metadata they provide.
Declaring and applying annotations
In Kotlin, annotations are used to associate additional metadata with declarations, like functions or classes. This metadata can be accessed by various tools that work with source code, compiled class files, or at runtime, depending on how the annotation is configured.
Applying annotations
To apply an annotation in Kotlin, you use the @
symbol followed by the annotation’s name at the beginning of the declaration you want to annotate. You can apply annotations to functions, classes, and other code elements. Let’s see some examples:
Here’s an example using the JUnit framework, where a test method is marked with the @Test
annotation:
import org.junit.*
class MyTest {
@Test
fun testTrue() {
Assert.assertTrue(true)
}
}
In Kotlin, annotations can have parameters. Let’s take a look at the @Deprecated
annotation as a more interesting example. It has a replaceWith
parameter, which allows you to provide a replacement pattern to facilitate a smooth transition to a new version of the API. The following code demonstrates the usage of annotation arguments, including a deprecation message and a replacement pattern:
@Deprecated("Use removeAt(index) instead.", ReplaceWith("removeAt(index)"))
fun remove(index: Int) { ... }
In this case, when someone uses the remove
function in their code, the IDE will not only show a suggestion to use removeAt
instead, but it will also offer a quick fix to automatically replace the remove
function with removeAt
. This makes it easier to update your code and follow the recommended practices.
Annotations in Kotlin can have arguments of specific types, such as primitive types, strings, enums, class references, other annotation classes, and arrays of these types. The syntax for specifying annotation arguments is slightly different from Java:
To specify a class as an annotation argument, use the ::class
syntax:
When you want to specify a class as an argument for an annotation, you can use the ::class
syntax.
@MyAnnotation(MyClass::class)
In this case, let’s say you have a custom annotation called @MyAnnotation
, and you want to pass a class called MyClass
as an argument to that annotation. In this case, you can use the ::class
syntax like this: @MyAnnotation(MyClass::class)
.
By using ::class
, you are referring to the class itself as an object. It allows you to pass the class reference as an argument to the annotation, indicating which class the annotation is associated with.
To specify another annotation as an argument, don’t use the @
character before the annotation name:
when specifying an annotation as an argument for another annotation, you don’t need to use the “@” symbol before the annotation name.
@Deprecated(replaceWith = ReplaceWith("removeAt(index)"))
fun remove(index: Int) { ... }
In the above example, the @Deprecated
annotation. It allows you to provide a replacement pattern using the ReplaceWith
annotation. In this case, you simply specify the ReplaceWith
annotation without the “@” symbol when using it as an argument for @Deprecated
.
By omitting the “@” symbol, you indicate that the argument is another annotation.
To specify an array as an argument, use the arrayOf
function:
if you want to specify an array as an argument for an annotation, you can use the arrayOf
function.
For example, let’s say you have an annotation called @RequestMapping
with a parameter called path
, and you want to pass an array of strings ["/foo", "/bar"]
as the value for that parameter. In this case, you can use the arrayOf
function like this:
@RequestMapping(path = arrayOf("/foo", "/bar"))
However, if the annotation class is declared in Java, you don’t need to use the arrayOf
function. In Java, the parameter named value
in the annotation is automatically converted to a vararg parameter if necessary. This means you can directly provide the values without using the arrayOf
function.
To use a property as an annotation argument, you need to mark it with a const modifier:
In Kotlin, annotation arguments need to be known at compile time, which means you cannot refer to arbitrary properties as arguments. However, you can use the const
modifier to mark a property as a compile-time constant, allowing you to use it as an annotation argument.
To use a property as an annotation argument, follow these steps:
- Declare the property using the
const
modifier at the top level of a file or inside an object. - Initialize the property with a value of a primitive type or a
String
.
Here’s an example using JUnit’s @Test
annotation that specifies a timeout for a test:
const val TEST_TIMEOUT = 100L
@Test(timeout = TEST_TIMEOUT)
fun testMethod() {
// Test code goes here
}
In this example, TEST_TIMEOUT
is declared as a const
property with a value of 100L
. The timeout
parameter of the @Test
annotation is then set to the value of TEST_TIMEOUT
. This allows you to specify the timeout value as a constant that can be reused and easily changed if needed.
Remember that properties marked with const
need to be declared at the top level of a file or inside an object, and they must be initialized with values of primitive types or String
. Using regular properties without the const
modifier will result in a compilation error with the message “Only ‘const val’ can be used in constant expressions.”
Annotation targets
In Kotlin, a single declaration can correspond to multiple Java declarations, each of which can have annotations. For example, a Kotlin property can correspond to a Java field, a getter, a setter, and a constructor parameter (in the case of a property declared in the primary constructor). In such cases, it’s important to specify which element should be annotated.
To specify the element to be annotated, you use a “use-site target” declaration. The use-site target is placed between the “@” sign and the annotation name, separated by a colon. For instance, if you want to apply the @Rule
annotation to the property getter, you would write @get:Rule
.
Let’s take an example using JUnit. In JUnit, you can specify a rule to be executed before each test method. The standard TemporaryFolder
rule is used to create files and folders that are automatically deleted when the test method finishes.
In Java, you would declare a public field or method annotated with @Rule
to specify the rule. However, if you annotate the folder
property in your Kotlin test class with @Rule
, you’ll encounter a JUnit exception saying “The @Rule ‘folder’ must be public.” This happens because @Rule
is applied to the field, which is private by default. To apply it to the getter, you need to write @get:Rule
explicitly.
Here’s an example:
class HasTempFolder {
@get:Rule
val folder = TemporaryFolder()
@Test
fun testUsingTempFolder() {
val createdFile = folder.newFile("myfile.txt")
val createdFolder = folder.newFolder("subfolder")
// ...
}
}
When you annotate a property with an annotation declared in Java, it is applied to the corresponding field by default. However, Kotlin also allows you to declare annotations that can be directly applied to properties.
The following is a list of supported use-site targets in Kotlin:
property
: Java annotations cannot be applied with this use-site target.field
: Field generated for the property.get
: Property getter.set
: Property setter.receiver
: Receiver parameter of an extension function or property.param
: Constructor parameter.setparam
: Property setter parameter.delegate
: Field storing the delegate instance for a delegated property.file
: Class containing top-level functions and properties declared in the file.
If you want to annotate a file with the file
target, the annotation needs to be placed at the top level of the file, before the package
directive. For example, @file:JvmName("StringFunctions")
changes the name of the corresponding class.
Unlike Java, Kotlin allows you to apply annotations to arbitrary expressions, not just class and function declarations or types. A common example is the @Suppress
annotation, which can be used to suppress a specific compiler warning within the annotated expression. Here’s an example that suppresses an unchecked cast warning for a local variable declaration:
fun test(list: List<*>) {
@Suppress("UNCHECKED_CAST")
val strings = list as List<String>
// ...
}
Note that IntelliJ IDEA can automatically insert this annotation for you when you encounter a compiler warning by pressing Alt-Enter and selecting Suppress from the intention options menu.
Controlling the Java API with annotations
Kotlin provides several annotations that allow you to control how Kotlin declarations are compiled to Java bytecode and interact with Java callers. These annotations serve various purposes, such as replacing Java keywords, changing method or field names, exposing methods as static Java methods, generating function overloads, or exposing properties as Java fields without getters or setters. Let’s go through them:
@Volatile
and @Strictfp
@Volatile
is used as a replacement for the Java keywordvolatile
, indicating that a property should be treated as volatile in Java.@Strictfp
is used as a replacement for the Java keywordstrictfp
, ensuring that a method or class adheres to strict floating-point arithmetic rules in Java.
@JvmName
@JvmName
allows you to change the name of a method or field that is generated from a Kotlin declaration when it is accessed from Java.- By default, Kotlin uses its own naming conventions, and
@JvmName
provides compatibility with existing Java code that expects different names.
@JvmStatic
@JvmStatic
is applied to methods within an object declaration or a companion object in Kotlin.- It exposes those methods as static Java methods, meaning they can be called directly on the class without needing an instance of the enclosing object or companion object.
@JvmOverloads
- When a Kotlin function has default parameter values,
@JvmOverloads
instructs the Kotlin compiler to generate additional overloaded versions of the function in the bytecode. - These generated overloaded versions provide options to Java callers to omit some or all of the optional parameters, making it easier to call the function from Java code.
@JvmField
@JvmField
is used to expose a property as a public Java field without generating the default getters and setters.- When applied to a property, Kotlin will generate a public Java field instead, allowing direct access to the field from Java code.
These annotations enhance the interoperability between Kotlin and Java by providing fine-grained control over how Kotlin declarations are compiled and accessed from Java. They help ensure seamless integration between the two languages and facilitate working with existing Java codebases in Kotlin projects.
Declaring annotations (Custom annotation)
In Kotlin, we can declare our own annotations using the annotation
modifier. Annotations allow you to associate metadata with declarations such as functions, classes, properties, or parameters. This metadata can be accessed by tools or frameworks that work with your code, enabling additional functionality or behavior. Here’s the general syntax for declaring an annotation in Kotlin:
annotation class MyAnnotation
Let’s take the example of the @JsonExclude
annotation, which is a simple annotation without any parameters:
annotation class JsonExclude
The syntax resembles a regular class declaration but with the annotation
modifier. Annotation classes are used to define metadata associated with declarations and expressions and cannot contain any code, so they don’t have a body.
For annotations that require parameters, you can declare the parameters in the primary constructor of the annotation class. Let’s consider the @JsonName
annotation as an example, which takes a name
parameter:
annotation class JsonName(val name: String)
Here, we use the regular primary constructor declaration syntax. It’s important to note that the val
keyword is mandatory for all parameters of an annotation class.
Now, let’s compare how the same annotation would be declared in Java:
/* Java */
public @interface JsonName {
String value();
}
In Java, the annotation has a method called value()
, whereas in Kotlin, the annotation has a property called name
. In Java, when applying an annotation, you need to explicitly specify names for all attributes except value
. In Kotlin, applying an annotation is similar to a regular constructor call. You can use named arguments to make the argument names explicit, or you can omit them. For example, @JsonName(name = "first_name")
is equivalent to @JsonName("first_name")
, as name
is the first parameter of the JsonName
constructor.
If you need to apply a Java annotation to a Kotlin element, you must use the named-argument syntax for all arguments except value
, which Kotlin also recognizes as a special case.
Annotation Parameters:
Annotations can have constructors that take parameters, as we saw in the above example, allowing you to customize their behavior for different use cases. Parameters can have default values, making them optional when applying the annotation. Parameters can be of the following types:
- Primitive types (e.g.,
Int
,String
,Boolean
) - Enum classes
- Class references
- Other annotation classes
- Arrays of the above types
Here’s an example of an annotation with parameters:
annotation class MyAnnotation(val value: String, val priority: Int = 1)
In the above example, MyAnnotation
takes two parameters: value
of type String
and priority
of type Int
. The priority
parameter has a default value of 1, making it optional when applying the annotation, which means it is optional to provide a value for priority
when applying the annotation. If a value is not explicitly provided, the default value of 1 will be used.
Instantiation
In Java, an annotation type is a form of an interface, so you can implement it and use an instance. For example, the following code defines an annotation type called InfoMarker
and then implements it in a class called MyClass
:
@interface InfoMarker {
String info() default "default";
}
class MyClass implements InfoMarker {
@Override
public String info() {
return "This is my info";
}
}
While, in Kotlin, you can call the constructor of an annotation class in arbitrary code( This means that you can create an instance of an annotation class anywhere in your code, not just in a class that implements the annotation.)and similarly use the resulting instance. For example, the following code defines an annotation class called InfoMarker
and then creates an instance of the annotation class in the main
function:
annotation class InfoMarker(val info: String)
fun main(args: Array<String>) {
val marker = InfoMarker("This is my info")
println(marker.info) // This is my info
}
The InfoMarker
annotation class has a single property called info
, which is of type String
. The main
function creates an instance of the InfoMarker
annotation class by calling the constructor with the value “This is my info”. The println
function then prints the value of the info
property.
As you can see, Kotlin’s approach to annotation instantiation is much simpler than Java’s. In Kotlin, you don’t need to implement an annotation interface; you can simply call the constructor of the annotation class and use the resulting instance. This makes it much easier to create and use annotations in Kotlin code.
Meta-annotations: controlling how an annotation is processed
Let’s discuss how to control the usage of annotations and how you can apply annotations to other annotations.
In Kotlin, just like in Java, you can annotate an annotation class itself. These annotations, which can be applied to annotation classes, are called meta-annotations. Meta-annotations control how the compiler processes annotations. Various frameworks, including dependency injection libraries, also use meta-annotations to mark annotations for different purposes.
One commonly used meta-annotation in the Kotlin standard library is @Target
. It specifies the valid targets for the annotated annotation. For example, in the declarations of JsonExclude
and JsonName
in JKid(simple JSON serialization/deserialization library for Kotlin), @Target
is used as follows:
@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude
The @Target
meta-annotation indicates the types of elements to which the annotation can be applied. If @Target
is not used, the annotation will be applicable to all declarations, which might not make sense in specific contexts. The AnnotationTarget
enum provides a range of possible targets for annotations, including classes, files, functions, properties, property accessors, types, expressions, and more. If needed, you can specify multiple targets like this: @Target(AnnotationTarget.CLASS, AnnotationTarget.METHOD)
.
The commonly used targets include:
CLASS
: Annotation can be applied to classes and interfaces.FUNCTION
: Annotation can be applied to functions and methods.PROPERTY
: Annotation can be applied to properties.FIELD
: Annotation can be applied to fields (backing fields of properties).ANNOTATION_CLASS
: Annotation can be applied to other annotations.PARAMETER
: Annotation can be applied to function parameters.CONSTRUCTOR
: Annotation can be applied to constructors.
Custom Meta-Annotations
In Kotlin, you can define your own meta-annotation by using the AnnotationTarget.ANNOTATION_CLASS
target. This allows you to create an annotation that can be used to annotate other annotations. Here’s an example:
@Target(AnnotationTarget.ANNOTATION_CLASS)
annotation class BindingAnnotation
In this example, we define a meta-annotation called BindingAnnotation
using the AnnotationTarget.ANNOTATION_CLASS
target. This means that BindingAnnotation
can be used to annotate other annotations.
However, if you want to use your annotation from Java code, annotations with the PROPERTY
target cannot be used directly. To make such annotations usable from Java, you can add an additional target called AnnotationTarget.FIELD
. This will allow the annotation to be applied to properties in Kotlin and to fields in Java. Here’s an example:
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FIELD)<br>annotation class BindingAnnotation
In this updated example, we added AnnotationTarget.FIELD
as an additional target for the BindingAnnotation
annotation. This enables the annotation to be used on properties in Kotlin and fields in Java, making it usable in both languages.
The @Retention annotation
Another important meta-annotation you might be familiar with from Java is @Retention
. It determines whether the annotation will be stored in the .class
file and whether it will be accessible at runtime through reflection. In Kotlin, the default retention is RUNTIME
, which means annotations are retained in .class
files and accessible at runtime. Therefore, the JKid annotations do not explicitly specify retention.
If you want to declare your own annotations with different retention policies, you can use @Retention
and specify the desired AnnotationRetention
value, such as SOURCE
, BINARY
, or RUNTIME
.
Here’s an example of specifying retention explicitly:
SOURCE
: This retention policy indicates that the annotation will only be retained in the source code and will not be included in the compiled .class files. It will not be accessible at runtime through reflection.
@Retention(AnnotationRetention.SOURCE)
annotation class MyAnnotation
BINARY
: This retention policy specifies that the annotation will be stored in the .class files, but it won’t be accessible at runtime through reflection.
@Retention(AnnotationRetention.BINARY)
annotation class MyAnnotation
RUNTIME
: This is the default retention policy in Kotlin. It means that the annotation will be stored in the .class files and will be accessible at runtime through reflection.
@Retention(AnnotationRetention.RUNTIME)
annotation class MyAnnotation
By explicitly specifying the desired
AnnotationRetention
value using@Retention
, you can control how your annotations are retained and accessed in Kotlin.
Repeatable annotations
In Kotlin, just like in Java, you can use repeatable annotations, which allow you to apply an annotation multiple times to a single code element. To make an annotation repeatable in Kotlin, you need to mark its declaration with the @kotlin.annotation.Repeatable
meta-annotation. This ensures that the annotation can be repeated both in Kotlin and Java.
The key difference between Kotlin and Java in terms of repeatable annotations is the absence of a containing annotation in Kotlin. In Java, a containing annotation is automatically generated by the compiler with a predefined name to hold the repeated annotations. However, in Kotlin, the compiler generates this containing annotation automatically with the name @Tag.Container
.
Here’s an example that demonstrates the usage of repeatable annotations in Kotlin:
@Repeatable
annotation class Tag(val name: String)
// The compiler generates the @Tag.Container containing annotation
In the example above, the @Tag
annotation is marked as repeatable. This means you can apply it multiple times to the same code element. The Kotlin compiler automatically generates the containing annotation @Tag.Container
to hold the repeated annotations.
If you want to specify a custom name for the containing annotation, you can use the @kotlin.jvm.JvmRepeatable
meta-annotation. Here’s an example:
@JvmRepeatable(Tags::class)
annotation class Tag(val name: String)
annotation class Tags(val value: Array<Tag>)
In this case, the @Tag
annotation is marked as repeatable using @JvmRepeatable
and the explicitly declared containing annotation class Tags
.
To extract repeatable annotations in Kotlin or Java using reflection, you can use the KAnnotatedElement.findAnnotations()
function. This function allows you to retrieve all the instances of a specific repeatable annotation applied to an element.
Overall, Kotlin supports repeatable annotations similarly to Java, but with a slight difference in how the containing annotation is handled.
Here’s a real-time example in Kotlin, it demonstrates the usage of repeatable annotations:
@Repeatable
annotation class Author(val name: String)
@Author("John")
@Author("Jane")
class Book(val title: String)
fun main() {
val bookClass = Book::class
val annotations = bookClass.annotations
for (annotation in annotations) {
if (annotation is Author) {
println("Author: ${annotation.name}")
}
}
}
In this example, we have a Book
class that represents a book. We want to annotate the class with the names of the authors. The Author
annotation is marked as repeatable using @Repeatable
.
In the main
function, we retrieve the annotations applied to the Book
class using reflection. We iterate over the annotations and check if they are instances of the Author
annotation. If they are, we print out the name of the author.
When you run this code, it will output:
Author: John
Author: Jane
As you can see, the Author
annotation is repeated twice on the Book
class, allowing us to specify multiple authors for a single book.
This example showcases how you can use repeatable annotations in Kotlin to add multiple instances of the same annotation to a code element, and then access those annotations at runtime using reflection.
Lambdas
Annotations, which are used to provide metadata or additional information about elements in your code, can also be applied to lambdas in Kotlin. When an annotation is used on a lambda, it is applied to the invoke()
method that represents the body of the lambda.
One example of using annotations on lambdas is in frameworks like Quasar, which utilize annotations for concurrency control. In the provided code snippet, an annotation called Suspendable
is used on a lambda expression.
annotation class Suspendable
val f = @Suspendable { Fiber.sleep(10) }
In this example, the Suspendable
annotation is applied to the lambda expression. It indicates that the code inside the lambda is suspendable, meaning it can be paused and resumed later. The specific behavior and implementation details of the Suspendable
annotation would be determined by the framework or library you’re using.
Please note that the
Fiber.sleep(10)
inside the lambda is just a fictional example and may not reflect actual usage. It’s meant to demonstrate that the lambda contains some code that could be annotated withSuspendable
for concurrency control purposes.
Conclusion
Kotlin annotations are a powerful tool for adding metadata and controlling the behavior of code. Whether it’s using built-in annotations or creating custom ones, annotations enable developers to express additional information and automate tasks. By understanding the syntax, usage, and advanced techniques of Kotlin annotations, you can enhance your codebase, improve documentation, and streamline development processes.
Remember, annotations are not just decorations; they are a valuable asset in your Kotlin programming arsenal. By mastering annotations, you can take full advantage of their capabilities and write more robust, maintainable, and expressive code.