article banner

Kotlin and Java interoperability: Properties and annotations

This is a chapter from the book Advanced Kotlin. You can find it on LeanPub or Amazon.

Kotlin introduced the property concept, which translates to Java getters, optional setters, fields, and delegates.

import kotlin.properties.Delegates.notNull class User { var name = "ABC" // getter, setter, field var surname: String by notNull() //getter, setter, delegate val fullName: String // only getter get() = "$name $surname" }
// Compiles to the analog of the following Java code
public final class User {
    // $FF: synthetic field
    static final KProperty[] $$delegatedProperties = ...

    @NotNull
    private String name = "ABC";
    
    @NotNull
    private final ReadWriteProperty surname$delegate;
    
    @NotNull
    public final String getName() {
        return this.name;
    }
    
    public final void setName(@NotNull String var1) {
        Intrinsics.checkNotNullParameter(var1, "<set-?>");
        this.name = var1;
    }
    
    @NotNull
    public final String getSurname() {
        return (String) this.surname$delegate
                .getValue(this, $$delegatedProperties[0]);
    }
    
    public final void setSurname(@NotNull String v) {
        Intrinsics.checkNotNullParameter(v, "<set-?>");
        this.surname$delegate
                .setValue(this, $$delegatedProperties[0], v);
    }
    
    @NotNull
    public final String getFullName() {
        return this.name + ' ' + this.getSurname();
    }
    
    public User() {
        this.surname$delegate = Delegates.INSTANCE.notNull();
    }
}

This creates a problem when we annotate a property because if a property translates to many elements under the hood, like a getter or a field, how can we annotate a concrete one on JVM? In other words, how can we annotate a getter or a field?

class User { @SomeAnnotation var name = "ABC" }

When you annotate a Kotlin element that is used to generate multiple Java elements, you can specify a use-side target for an annotation. For instance, to mark that SomeAnnotation should be used for a property field, we should use @field:SomeAnnotation.

class User { @field:SomeAnnotation var name = "ABC" }

For a property, the following targets are supported:

  • property (annotations with this target are not visible to Java)
  • field (property field)
  • get (property getter)
  • set (property setter)
  • setparam (property setter parameter)
  • delegate (the field storing the delegate instance for a delegated property)
annotation class A annotation class B annotation class C annotation class D annotation class E class User { @property:A @get:B @set:C @field:D @setparam:E var name = "ABC" }
// Compiles to the analog of the following Java code
public final class User {
    @D
    @NotNull
    private String name = "ABC";
    
    @A
    public static void getName$annotations() {
    }
    
    @B
    @NotNull
    public final String getName() {
        return this.name;
    }
    
    @C
    public final void setName(@E @NotNull String var1) {
        Intrinsics.checkNotNullParameter(var1, "<set-?>");
        this.name = var1;
    }
}

When a property is defined in a constructor, an additional param target is used to annotate the constructor parameter.

class User( @param:A val name: String )

By default, the annotation target is chosen according to the @Target annotation of the annotation being used. If there are multiple applicable targets, the first applicable target from the following list is used:

  • param
  • property
  • field

Note that property annotations without an annotation target will by default use property, so they will not be visible from Java reflection.

annotation class A class User { @A val name = "ABC" }
// Compiles to the analog of the following Java code
public final class User {
    @NotNull
    private String name = "ABC";
    
    @A
    public static void getName$annotations() {
    }
    
    @NotNull
    public final String getName() {
        return this.name;
    }
}

An annotation in front of a class annotates this class. To annotate the primary constructor, we need to use the constructor keyword and place the annotation in front of it.

annotation class A annotation class B @A class User @B constructor( val name: String )
// Compiles to the analog of the following Java code
@A
public final class User {
    @NotNull
    private final String name;
    
    @NotNull
    public final String getName() {
        return this.name;
    }
    
    @B
    public User(@NotNull String name) {
        Intrinsics.checkNotNullParameter(name, "name");
        super();
        this.name = name;
    }
}

We can also annotate a file using the file target and place an annotation at the beginning of the file (before the package). An example will be shown in the JvmName section.

When you annotate an extension function or an extension property, you can also use the receiver target to annotate the receiver parameter.

annotation class Positive fun @receiver:Positive Double.log() = ln(this) // Java alternative public static final double log(@Positive double $this$log) { return Math.log($this$log); }

Static elements

In Kotlin, we don’t have the concept of static elements, so use object declarations and companion objects instead. Using them in Kotlin is just like using static elements in Java.

import java.math.BigDecimal class Money(val amount: BigDecimal, val currency: String) { companion object { fun usd(amount: Double) = Money(amount.toBigDecimal(), "PLN") } } object MoneyUtils { fun parseMoney(text: String): Money = TODO() } fun main() { val m1 = Money.usd(10.0) val m2 = MoneyUtils.parseMoney("10 EUR") }

However, using these objects from Java is not very convenient. To use an object declaration, we need to use the static INSTANCE field, which is called Companion for companion objects.

// Java
public class JavaClass {
    public static void main(String[] args) {
        Money m1 = Money.Companion.usd(10.0);
        Money m2 = MoneyUtils.INSTANCE.parseMoney("10 EUR");
    }
}

It’s important to know that to use any Kotlin element in Java, a package must be specified. Kotlin allows elements without packages, but Java does not.

To simplify the use of these object declaration[^53_1] methods, we can annotate them with JvmStatic, which makes the compiler generate an additional static method to support easier calls to non-Kotlin JVM languages.

// Kotlin class Money(val amount: BigDecimal, val currency: String) { companion object { @JvmStatic fun usd(amount: Double) = Money(amount.toBigDecimal(), "PLN") } } object MoneyUtils { @JvmStatic fun parseMoney(text: String): Money = TODO() } fun main() { val money1 = Money.usd(10.0) val money2 = MoneyUtils.parseMoney("10 EUR") }
// Java
public class JavaClass {
    public static void main(String[] args) {
        Money m1 = Money.usd(10.0);
        Money m2 = MoneyUtils.parseMoney("10 EUR");
    }
}

JvmField

As we’ve discussed already, each property is represented by its accessors. This means that if you have a property name in Kotlin, you need to use the getName getter to use it in Java; if a property is read-write, you need to use the setNamesetter.

// Kotlin class Box { var name = "" }
// Java
public class JavaClass {
    public static void main(String[] args) {
        Box box = new Box();
        box.setName("ABC");
        System.out.println(box.getName());
    }
}

The name field is private in Box, so we can only access it using accessors. However, some libraries that use reflection require the use of public fields, which are provided using the JvmField annotation for a property. Such properties cannot have custom accessors, use a delegate, be open, or override another property.

// Kotlin class Box { @JvmField var name = "" }
// Java
public class JavaClass {
    public static void main(String[] args) {
        Box box = new Box();
        box.name = "ABC";
        System.out.println(box.name);
    }
}

When the JvmField annotation is used for an annotation in an object declaration or a companion object, its field also becomes static.

// Kotlin object Box { @JvmField var name = "" }
// Java
public class JavaClass {
    public static void main(String[] args) {
        Box.name = "ABC";
        System.out.println(Box.name);
    }
}

Constant variables do not need this annotation as they will always be represented as static fields.

// Kotlin class MainWindow { // ... companion object { const val SIZE = 10 } }
// Java
public class JavaClass {
    public static void main(String[] args) {
        System.out.println(MainWindow.SIZE);
    }
}

Using Java accessors in Kotlin

Kotlin properties are represented by Java accessors. A typical accessor is just a property name capitalized with a get or set prefix. The only exception is Boolean properties prefixed with "is", whose getter name is the same as the property name, and its setter skips the "is" prefix.

class User { var name = "ABC" var isAdult = true }
// Java alternative
public final class User {
    @NotNull
    private String name = "ABC";
    private boolean isAdult = true;
    
    @NotNull
    public final String getName() {
        return this.name;
    }

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

    public final boolean isAdult() {
        return this.isAdult;
    }

    public final void setAdult(boolean var1) {
        this.isAdult = var1;
    }
}

Java getters and setters can be treated as properties in Kotlin. A getter without a setter is treated like a val property. When there is both a getter and a setter, they are treated together like a var property. A setter without a getter cannot be interpreted as a property because every property needs a getter.