In Kotlin code, you should have seen an annotation like @file:JvmName(…) Normal annotations do not have a prefix like @file:, and there is no such syntax in the Java language. So, what does this really do? Because of its special function, I call it “positional annotation”.

Kotlin is a programming language that simplifies syntax to the extreme. Let’s take a look at some simple code:

class Person {
    var name: String? = null
}
Copy the code

This extremely simple code, processed by the Kotlin compiler, is equivalent to the following Java code:

public final class Person { @Nullable private String name; @Nullable public final String getName() { return this.name; } public final void setName(@Nullable String var1) { this.name = var1; }}Copy the code

Although in Kotlin, it looks like only one member variable is declared, in fact the compilation not only declares a member variable name, but also generates the corresponding setter/getter method.

At this point, the question is, what happens if we add an annotation above the Name attribute of the Person class?

class Person {
   @Callable
   var name: String? = null
}
Copy the code

As we said, the actual generated bytecode contains setter/getter methods, so there are four possible locations for this annotation:

  • Property (member variable name)
  • A setter method
  • Setter method parameter name
  • Getter method

The specific possible locations are shown in the figure below:

Public final class Person {@callable @nullable private String name; // setMethod @callable public final void setName(/* setmethod */ @nullable String var1) {this.name = var1; } // GetMethod @callable @nullable public Final String getName() {return this.name; }}Copy the code

The compiler is confused at this point, and it can’t determine exactly where you want the annotations to appear. So what exactly does the Kotlin compiler do in this case? Those of you who are interested can do the experiment yourself.

So, is there a way to make annotations appear exactly where they are? The answer is: of course! Positional annotations are designed to solve this problem.

We’ll annotate the above code with a position comment and change it to look like this:

class Person {
    @field:Callable
    var name: String? = null
}
Copy the code

By adding the @field positional annotation, the @callable annotation will appear exactly where the attribute is defined, as shown below:

Public final class Person {@callable @nullable private String name; public final void setName(@Nullable String var1) { this.name = var1; } @Nullable public final String getName() { return this.name; }}Copy the code

The position notes for the other three positions are:

  • Set: corresponds to the setter method position
  • Get: indicates the location of the getter method
  • Setparam: corresponding setter method parameter position

In addition, Kotlin provides the following positional annotations for different usage scenarios:

@file:

This annotation is used at the file level. Each Kotlin file corresponds to one or more Java classes. When corresponding to a class, you can change the name of the generated Java class by adding this annotation and using it in conjunction with the @jvmName annotation from the previous lesson.

You can understand that the actual position of the annotation is the resulting Java class.

@file:JvmName("FooKt")
fun foo() {
    println("Hello, world...")
}
Copy the code

The resulting code looks like this, with the generated class name being exactly what was written at the top of the annotation:

@JvmName("FooKt") public final class FooKt { public final void foo() { ... }}Copy the code

@param:

The purpose of this annotation is to position the occurrence of the annotation on the constructor parameter.

As you know, in Kotlin, if you add var or val before a constructor argument, the corresponding property, setter, and getter methods are generated in the corresponding class.

This annotation was created to make it appear exactly where its constructor arguments are!

Let’s continue with an example:

class Person(@param:Callable var name: String)
Copy the code

After the above positional annotations are added, the resulting annotations will appear at the constructor argument as follows:

public final class Person { @NotNull private String name; @NotNull public final String getName() { return this.name; } public final void setName(@NotNull String var1) { this.name = var1; } public Person(@callable @notnull String name) {super(); this.name = name; }}Copy the code

@property:

This is a special position annotation that is not visible to the Java side and represents the position of the Property object corresponding to the Property. So this is kind of abstract, so let’s look at an example and see what a Property is.

Continuing with the Person class as an example, we access it with the following code:

Fun main(args: Array<String>) {val person = person ("Scott") val propertyName = person::name falsee println("${propertyName.name}: ${propertyName.isConst}") }Copy the code

In the above code, propertyName corresponds to the Property instance of the Name Property in the Person class. In simple terms, the Property stores information about the corresponding Property and represents the current Property. Property lets you get information about the current Property (the name of the variable, whether it’s constant, whether it’s delayed initialization, and so on).

If you add a positional annotation @property: in front of the constructor’s name, the location of the annotation generated is a little more obscure. The only way to access this annotation is through its Property instance. Let’s try it out:

class Person(@property:Callable var name: String)
Copy the code

The only way to access this annotation is through its Property instance, and it is not accessible on the Java side:

fun main(args: Array<String>) {val person = person ("Scott") val propertyName = person::name // The only way to access this annotation println(propertyName.annotations.find { it.annotationClass == Callable::class }) }Copy the code

So, in terms of bytecode, where exactly does this annotation appear? Let’s decompile it:

public final class Person { @NotNull private String name; // The annotation appears here, @callable public static void name$Annotations () {} @notnull public final String getName() {return this.name;  } public final void setName(@NotNull String var1) { this.name = var1; } public Person(@NotNull String name) { super(); this.name = name; }}Copy the code

You can see that the annotation appears on a static method named using the attribute name suffix **$and annotations** generated by the Kotlin compiler. This is a special method of the Kotlin compiler convention, which is exactly accessible through the Property instance.

With this convention named, the fact that the Java side can call on the annotation in a special way is not strictly accurate.

@ receiver:

This is also a very special positional annotation. Kotlin supports extension functions, which extend functions or attributes of an existing class without inheritance. An important concept in extensions is receiver, which refers to the instance itself of the extended class.

The problem is that the extension doesn’t change the code of the original class, so how to put annotations in the receiver location seems like an impossible task.

This brings us to the implementation principle of the extension, which actually corresponds to a global function in Kotlin. When converted to bytecode, the first argument of the function is the receiver itself. This may sound abstract, but let’s look at an example:

We’ll start by adding the extension sayHi to the Person class:

fun Person.sayHi(greet: String) {
    println("$greet, $name")
}
Copy the code

Then decompile to see the resulting Java code:

public static final void sayHi(@NotNull Person $receiver, @NotNull String greet) {
  String var2 = greet + ", " + $receiver.getName();
  System.out.println(var2);
}
Copy the code

As you can see, the Kotlin compiler generates a static method whose first argument is receiver, corresponding to the extension class instance itself, and whose second argument is the actual argument to the extension function.

This is how The Kotlin extension is implemented, which is ultimately achieved by adding a static function whose first argument always points to an instance of the extended class, the receiver. After we add the @receiver location annotation, the location generated by the annotation will appear at the first parameter of the extension function, like the following:

public static final void sayHi(@Callable @NotNull Person $receiver, @NotNull String greet) {
  String var2 = greet + ", " + $receiver.getName();
  System.out.println(var2);
}
Copy the code

This is where the @Receiver location annotation comes in, and it’s easy to see why this annotation is useful when you understand how extension functions work.

@delegate

This is the last positional annotation of the day, and it’s one of the more difficult positional annotations to understand because there is no such concept in the Java language. Proxy patterns are popular in Kotlin, where they are easily implemented using the BY keyword.

There is a similar problem here. As we said earlier, declaring a property in a Kotlin class actually generates setter/getter methods at the same time, so there are only three possible places for annotations besides the property (setter/getter/setter parameters). If the property itself is generated as a proxy, there is one more location: the location of the proxy class property.

This may not be intuitive, but let’s use the official lazy implementation as an example.

We added a proxy attribute gender to the Person class:

class Person(var name: String) {
    @delegate:Callable
    val gender by lazy { "male" }
}
Copy the code

As usual, we will decompile the Java code directly and analyze it:

@callable @notnull Private Final Lazy gender$delegate; @callable @notnull private final Lazy gender$delegate; @NotNull public final String getGender() { Lazy var1 = this.gender$delegate; return (String)var1.getValue(); } public Person(@NotNull String name) { super(); this.name = name; this.gender$delegate = LazyKt.lazy((Function0)null.INSTANCE); }}Copy the code

From the code above, we can clearly see that the Kotlin compiler generates the real proxy class instance property in the class, and that the value of gender is actually obtained from the proxy object. The purpose of this positional annotation is to place the annotation precisely above the instance properties of the proxy class.

Default location annotation priority

Positional annotations are not mandatory in Kotlin, we do not have to add positional annotations. In the absence of positional annotations, Kotlin will place annotations in the specified location according to the following priority (if annotations can appear in more than one location at a time) :

param > property > field

Best practices

These are all the positional annotations we can use in Kotlin, because Kotlin simplifies syntax to the extreme and we need these annotations to tell the compiler exactly where to put annotations. If you need to add annotations to your code, always add positional annotations so that they can be placed exactly where you want them to be and avoid unnecessary complications.

For more technical articles, please follow ouyang Feng Studio on wechat.

To participate in Kotlin technical discussion, please add the only official QQ communication group: 329673958