Everything about JVMField, JVMOverloads, JVMName, and JVMStatic annotations in Kotlin

Everything about JVMField, JVMOverloads, JVMName, and JVMStatic annotations in Kotlin

The mentioned four JVMField, JVMOverloads, JVMName, and JVMStatic annotations are used for easier and convenient inter-operability between Kotlin and JAVA code.
Used in examples like:

  1. We can create the object of a Kotlin class in our JAVA files.

  2. Instances of a Kotlin class can be seamlessly created and operated in Java methods.

There is a very high chance that you have already used a JAVA-based library while working with Kotlin or Android. Some of these libraries might expect some different behavior from your Kotlin compiled code(they are expecting JAVA) that can lead to an unexpected error, but you can still magically modify your compiled code by using these annotations and have a seamless experience working with both Kotlin and JAVA.

Note: I have never used these Annotations in any of my projects till now, but they are really interesting and important concepts to understand as a Kotlin/Android developer. It’s better to be safe than sorry when it comes to Java codebases lurking in the shadows.

JVMField:

What:

According to docs, it is an annotation class that Instructs the Kotlin compiler not to generate getters/setters for this property and expose it as a field.
To understand this, you need to first understand how the Kotlin fields work for JAVA.

A Kotlin property is compiled to the following Java elements:

  • a getter method, with the name calculated by prepending the get prefix

  • a setter method, with the name calculated by prepending the set prefix (only for var properties)

  • a private field, with the same name as the property name (only for properties with backing fields)

For example,

class SomeClass(var firstName: String)

compiles to the following Java declarations:

//In java
private String firstName;

public String getFirstName() {
    return firstName;
}
public void setFirstName(String firstName) {
    this.firstName = firstName;
}

That means when using your Kotlin classes in JAVA code, you can only access functions and not fields.

class KotlinClass {
    val normalProperty: String = "This is a normal property"
}

Used In Java classes like:

KotlinClass kotlinObject = new KotlinClass();
// Accessing the property requires a getter
String value = kotlinObject.getNormalProperty();

But, we can use our JVMField annotation to instruct our compiler to treat Kotlin fields as Java fields.

class KotlinClass {
    @JvmField
    val exposedField: String = "This is a JVM field"
}

Now, in Java:

KotlinClass kotlinObject = new KotlinClass();
// Accessing the field directly
String value = kotlinObject.exposedField;

Use:

It can be used with

  1. Top-Level Objects

  2. Companion objects

  3. Parameters of Primary Constructors

Why:

1. Potential Performance Optimization:

  • In rare cases where getters/setters might introduce a performance overhead, @JvmField can provide a slight improvement by directly exposing the field, eliminating the need for method calls.

2. Workaround for Specific Java Libraries:

  • When working with certain Java libraries that have strict expectations about field access and don’t seamlessly interact with Kotlin properties, @JvmField can provide a workaround to ensure compatibility.

3. Potential Memory Optimization:

  • In certain scenarios involving large data structures, avoiding the overhead of getters/setters for frequently accessed properties can lead to modest memory savings.

Drawbacks/Cautions:

  1. Compromised Null-Safety:
  • Kotlin’s property features ensure null safety, preventing null values from being assigned to non-nullable properties. However, fields exposed with @JvmField can be assigned null from Java code, potentially leading to NullPointerExceptions if not handled carefully.

2. Potential for Tight Coupling:

  • Exposing fields directly can create tighter coupling between classes, as they become more dependent on the internal implementation details of each other. This can make code more difficult to change and refactor in the future.

JVMOverloads:

What:

According to docs, it is an annotation class that instructs the Kotlin compiler to generate overloads for the function that substitutes default parameter values.

This explains it all but if you still didn’t get it, I assume you are already familiar with Kotlin and the default parameters in Kotlin functions, let us see an example…

fun draw(label: String, lineWidth: Int = 1, color: String = "red")

This is a normal function in Kotlin which 2 parameters with default values. Now, since Java does not support default values, we will get the same function with 3 parameters in Java.

@JvmOverloads fun draw(label: String, lineWidth: Int = 1, color: String = "red")

But, after applying the annotation your compiler will generate multiple overloads of these functions, as shown below…

void draw(String label, int lineWidth, String color) { }
void draw(String label, int lineWidth) { }
void draw(String label) { }

Use:

It can be used with

  1. Non-static methods

  2. Static methods

  3. Constructors

Drawbacks:

  1. Increased Binary Size: Generating multiple overloaded versions of functions can lead to increased binary size, especially in large projects. This can impact the performance and memory footprint of your application.

  2. Potential Ambiguity: In certain cases, using @JvmOverloads can lead to ambiguity, especially when there are multiple overloaded versions of the same function with different combinations of parameters of the same type. This can cause errors.

  3. Debugging Complexity: With multiple overloaded versions of functions, debugging can become more complex, as it may not always be clear which version of the function is being called at runtime.

JVMName:

According to docs, it is used to specify the name of a class or method that is generated from one element.

This is not enough to understand so let us take an example, given two functions in Kotlin:

fun List<Any>.filler(): List<Int> = listOf(0)

fun <T> List<T>.filler() : List<String> = listOf("0")

They have the same name but they are used with different types of lists.

In Kotlin, by default always the first function will get called, and if you want to call the second one then explicitly define the T type while using.

list.filler()//Calls first
list.filler<String>()//Calls second

In Java, it will show us an “Ambiguous method call” error, so in cases like these, you can consider using the JVMName annotation in one or both of the functions.

@JvmName("fillerString")
fun <T> List<T>.filler() : List<String> = listOf("0")

Another example is having a field that exposes the same name to our Java classes.

val x: Int
    get() = 15

fun getX() = 10

Here, for field X, we can have a getter and setter in our Java class, and for the getter, it will be getX() method name, but the problem is you can see that a method with the same name already exists, so we can use the JVMName annotation to define a different name for our Java class.

val x: Int
    @JvmName("getX_prop")
    get() = 15

Extending this, we can also set different names for getter and setter:

@get:JvmName("x")
@set:JvmName("changeX")
var x: Int = 23

You can also use it on a file name.

JVMStatic:

According to docs, it Specifies that an additional static method needs to be generated from this element if it’s a function. If this element is a property, additional static getter/setter methods should be generated.

It simply means, that if you were thinking that defining a companion object in Kotlin is equal to the static object or function in JAVA, then you are somewhat wrong. It works differently.

Once we define a companion object in Kotlin, it will not directly expose that field or function in Java classes as a static field, but it will only expose a Companion class object, which will then help us access those methods.

Example:

class KotlinClass {
    companion object {
        fun first(): Unit {}

        fun second(): Unit {}
    }
}
public class JavaClass {
    public static void main(String[] args) {
        KotlinClass.first();//Compilation error
        KotlinClass.Companion.second();//Works this way
    }
}

So, now you have an idea of why JVMStatic annotation exists, we can use this to annotate the functions and expose them directly as static values in Java classes.

@JvmStatic
fun first(): Unit {}
public static void main(String[] args) {
    KotlinClass.first();//Works fine
    KotlinClass.Companion.second();//Also works fine
}

This also works for fields:

class KotlinClass {
    companion object {
        @JvmStatic
        var a = 1
    }
}
public class JavaClass {
    public static void main(String[] args) {
        KotlinClass.getA();
    }
}

But, this was a field, right? What if we don’t want these getters and setters, we already know we can use the JVMField annotation…

But, we can not use both of these annotations at one time. So, I am using only JVMField.

class KotlinClass {
    companion object {
        @JvmField
        var a = 1
    }
}

And now this annotation also helps us expose this as a static field directly.

public class JavaClass {
    public static void main(String[] args) {
        int a = KotlinClass.a;
    }
}

JVMWildcard:

As always, there is something extra to learn in my articles, and here is this annotation “JvmWildcard”.

I'm not jumping deeply into the explanation, just a simple use case when we are using inheritance in kotlin and expecting a child class in parent definition, then we can use this for generating <? extends Something> type.

In Koltin:

fun boxDerived(value: Derived): Box<Derived> = Box(value)
fun unboxBase(box: Box<Base>): Base = box.value

//This is fine to use in kotlin
unboxBase(boxDerived(Derived()))

But in Java, it is not gonna work…

Box<Derived> boxDerived(Derived value) { ... }
Base unboxBase(Box<Base> box) { ... }

because in Java the class Box is invariant in its parameter T, and thus Box<Derived> is not a subtype of Box<Base>.

To make this work in Java, you would have to define unboxBase as follows:

Base unboxBase(Box<? extends Base> box) { ... }

This declaration uses Java’s wildcard types (? extends Base) to emulate declaration-site variance through use-site variance.

To make it possible, simply use this in Kotlin:

fun boxDerived(value: Derived): Box<@JvmWildcard Derived> = Box(value)
// is translated to
// Box<? extends Derived> boxDerived(Derived value) { ... }

That’s all for today.

I hope you learned something new from this article, if yes then make sure to Like this and follow “Sagar Malhotra” for more such useful content.