The not-so-obvious Kotlin. Field annotations and modifiers.

Nikolay Miroshnychenko
5 min readFeb 26, 2021
https://unsplash.com/@federicorespini

Recently I’ve taken the Kotlin for Java developers course developed by JetBrains. It’s an excellent course for Java devs that want to know all about Kotlin’s features, and they make a really great job at explaining things. However, I’ve felt that some points could have been explored more as they’re not really obvious for people that are transitioning from Java. I’m going to pinpoint these things and do my best at explaining these in a series of articles. These articles will not go in into explaining concepts that are straightforward and easy to grasp, so don’t expect a detailed explanation of Kotlin here, but no advanced stuff here either.

The first point that I want to cover is fields and variables. How can you declare a static field vs a regular field? How would you declare a compile-time constant? What if you don’t want getters and setters to be generated? What is inlining? I’ll try to answer these questions below.

Generating the right field types in Kotlin is all about using the correct mix of annotations and modifiers. Let’s look at the annotations first:

@JvmField for static fields or exposing the property

Instructs the Kotlin compiler not to generate getters/setters for this property and expose it as a field.

Getters and setters will be automatically generated for each field of your class. This might not obvious because in Kotlin we usually reference the fields as if they’re public — whereas “under the hood” a getter and setter will be called each time.

@JvmField is also obscure because it will produce a static or a regular field depending on where you put it in Kotlin. There are two cases:

  1. You annotate a field in a plain old class and you get a regular field just without any getter or setter as explained above.
  2. You annotate a field in an object or a companion object and you will get a static field.

Let’s see this in action. Let’s build a class Car:

class Car {
@JvmField
val wheels = "wheels"
val windows = "windows"

object Engine {
@JvmField
val oil = "oil"
val valves = "valves"
}

companion object {
@JvmField
val transmission = "transmission"
val lights = "lights"
}
}

Here’s how we would access each of the properties of this class in Java:

Car car = new Car();

/*
With @JvmField
*/

car.wheels;
// no getter / setter generated for the field

Car.Engine.oil;
// static field is generated for the inner static class

Car.transmission;
// static field generated for companion object


/*
Without @JvmField
*/

car.getWindows();
//getter / setter generated

Car.Engine.INSTANCE.getValves();
// accessed via the instance + getter generated

Car.Companion.getLights();
// accessed via the companion object instance + getter generated

@JvmStatic for static methods

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.

You’re probably going to use this annotation rarely, as this might provide value only in case you’re interacting with Java in your code. However, it’s worth knowing about this one.

Kotlin doesn’t have static functions, so whenever you declare a companion object and want to call the enclosing function from Java — you would need to make a similar call:

SomeClass.Companion.someMethod()

which is a bit cumbersome. Now if you annotate someMethod() with @JvmStatic — the compiler will generate static methods which would allow you to call the function just like a regular static function from Java:

SomeClass.someMethod()

const val for inlining a value

At first, it seems that there is no point in using const modifier if we have val, right? But there is an optimization going on under the hood in the case of const val, something called inlining. Suppose you have some Kotlin code like this:

const val path = "myComputer/coding"
class PathFinder {
fun getAbsPath(): String {
return "$path + /someOtherDirectory"
}
}

If we decompile this to bytecode and compile it back in Java (here’s a good article on how to do that in case you want to try), we should get something like this:

class PathFinder {
public String getAbsPath() {
return "myComputer/coding/someOtherDirectory"
}
}

As you can see this can make an important optimization difference if getAbsPath() is referenced often. So, even though you can use val everywhere instead of const — it’s important to know what optimization const brings.

Everything about a field can/has to be specified inside the primary constructor

Another not-so-obvious thing about Kotlin would be the fact that everything about the property has to/can be specified inside the primary constructor unless you have some custom logic further down of course. It’s convenient once you get used to it, but not really obvious in the beginning in my opinion. Let’s see this in our Car class.

Adding a parameter to the primary constructor without any modifiers doesn’t create any properties as expected:

class Car(airbag: String)

So we can’t access it in any way unless we do some initialization in the init {} block. Now let’s add a var modifier:

class Car(var airbag: String)

Now we can do this in Java:

car.getAirbag();
car.setAirbag("airbag");

And now let’s add @JvmField annotation:

class Car(@JvmField var airbag: String)

As expected, we get this on the Java side:

car.airbag;

Same thing for overriding a field. You can specify it in the constructor

interface Vehicle  {
val wheelCount: Int
}
class Car(override val wheelCount: Int) : Vehicle {}

Conclusion

Kotlin annotations and their mix with the modifiers were a bit vague for me in terms of what they create on the Java side at first. Because Kotlin is such a powerful and concise language I think it’s important to understand what’s going on when we apply certain annotations and modifiers.

That’s pretty much it in terms of what I wanted to cover on this topic. I hope I helped you get a clearer picture of this matter. Stay tuned for further articles on Kotlin’s non-obvious parts.

Thanks for reading. If you found this post valuable, please recommend it (the little handclap) so it can reach others.

--

--

Nikolay Miroshnychenko

Android engineer. Learning daily and sharing my knowledge in the process. Into mobile, computer science, and the brain.