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.
// 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?
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
.
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)
// 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.
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.
// 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.
// 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.
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.
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.
// 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 setName
setter.
// 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.
// 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.
// 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.
// 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.
// 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.