7 minutes read

We've already learned that annotations provide metadata for our code and that they are helpful and even essential when you use different frameworks, like Spring/Spring Boot. We've also talked about how to apply them and how to write our own custom annotations. In this topic, we'll learn to apply annotations in your source code to specific elements of the compiled bytecode.

Annotation and Java byte code

Now, let's look at this example:

class Cat(val name:String)

The name property in the primary constructor has multiple Java elements corresponding to such Kotlin elements as get, set, field, etc.

Annotation and Java byte code

That means that there may be multiple possible locations for the annotation in the generated Java bytecode. This is a problem, as most libraries and frameworks are made for Java. Kotlin, however, is different from Java: for example, you don't need to write get and set methods in Kotlin, as they will be generated automatically and you can customize them later, as you already know.

To get a predictable result from a library or framework, you need to specify which element the framework will work with.

The solution is use-site targets. In this topic, you will see some Java code, and I don't want you to get scared if you haven't worked with Java before. All languages have the same concepts, and we only need to see how our Kotlin code can be converted to Java code and why this matters when we deal with frameworks and libraries.

Now, don't worry, examples will explain it all, so let's get started.

Use-site targets

We can specify how exactly an annotation should be generated by using the following syntax:

using syntax to generate annotation

Here is an example:

annotation class Cool

// annotate Java getter
class Cat(@get:Cool val name: String)

We can apply multiple annotations to the same target by putting these annotations in brackets separated by a space :

// No commas needed to separate annotations
class Cat(@get:[Cool Wild] var name: String)

annotation class Cool
annotation class Wild

Annotation target

Note that the same syntax is used for targeting the whole file:

@file:Suppress("unused")

In the same manner, we can annotate the receiver parameter of an extension function:

fun @receiver:Cool String.capitalize() = println(this.uppercase())

Finally, notice that specifying a use-site target is optional and you should use it only when you need to target a specific element.

A legitimate question is: which target will be used if we don't specify a use-site target? According to the official documentation, it will be chosen according to the applicable target annotation specified by @Target. But what if there is more than one applicable target? In that case, the first applicable target from the following list is used:

  • param

  • property

  • field

The above paragraph might sound confusing, so let's discuss the case in more detail and take a look at an example:

@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER)
annotation class Cool


class Cat(@Cool var name: String)

In the above code, we have three applicable targets in @Target, and as we don't specify a particular target, there are many elements the annotation can be applied to.

What specific location in the corresponding Java code can we expect if we don't use annotation use-site targets?

Here is the corresponding Java code after decompilation – see if your guess is correct:

   @NotNull
   private String name;

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final void setName(@NotNull String var1) {
      Intrinsics.checkNotNullParameter(var1, "<set-?>");
      this.name = var1;
   }

   public Cat(@Cool @NotNull String name) {
      Intrinsics.checkNotNullParameter(name, "name");
      super();
      this.name = name;
   }
}

Did you expect "set" ? No, the annotation was applied to the parameter, not a property setter. Why? First, the compiler looks at @Target and finds three applicable targets, then it applies the annotation to the first applicable one from the above-mentioned list (param, property, field).

According to the documentation, the full list of supported use-site targets is the following:

  • file

  • property (annotations with this target are not visible to Java)

  • field

  • get (property getter)

  • set (property setter)

  • receiver (receiver parameter of an extension function or property)

  • param (constructor parameter)

  • setparam (property setter parameter)

  • delegate (the field storing the delegate instance for a delegated property)

Annotation: special cases

We have learned how to apply annotation to different elements, but there are some special cases you should be aware of.

If you want to apply an annotation to the primary constructor of a class, then you must use the constructor keyword and put the annotation before it:

class Cat @Cool constructor(name: String)

We can also use annotations on lambdas – they will be applied to the invoke() method into which the lambda body is generated.

val catName = @Cool { println("Bossy Cat") }

Conclusion

In this topic, we've learned how to use annotation use-site targets and how to apply annotations to a specific target. We've discussed these possible targets as well as some special cases and learned how to apply annotation to the primary constructor and a lambda expression. Now, let's have some practice.

9 learners liked this piece of theory. 2 didn't like it. What about you?
Report a typo