The not-so-obvious Kotlin. Field annotations and modifiers.
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:
- You annotate a field in a plain old class and you get a regular field just without any getter or setter as explained above.
- 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.