There has recently been a discussion online about the best approach for storing global constants, or
public static final
fields in Javaspeak. This short article describes available options and points
out some of the pitfalls you can run into. But before we start, let’s talk a bit about decompiling
Kotlin code.
Decompiling Kotlin
If you’ve used Kotlin for some time you should’ve noticed that the language is a wonderful
boilerplate reduction tool. Kotlin makes it easy to express complex things with simple code, with
compiler doing the dirty work. A great example is the data class
feature, which allows you to
easily substitute tens of lines of Java code with just a single line of Kotlin. However, as we all
know, with great power comes great responsibility. It’s not hard to make Kotlin compiler produce
suboptimal bytecode, and, especially if you’re doing Kotlin for Android, you have to be aware of the
number of classes, methods and object allocations that your code will produce. Luckily, JetBrains
has us covered with a decompiler tool integrated into Android Studio (and IntelliJ IDEA of course),
that helps examine the bytecode, and even produces similar Java code. The latter makes it easy to
optimize Kotlin.
There’s a number of great resources online on the topic of decompiling Kotlin, so I won’t go into much detail here and will just share some links:
- “Zero boilerplate delegation in Kotlin” by Piotr Ślesarew provides a detailed explanation on how to use the decompiler tool
- “Kotlin: Uncovered”, both the Droidcon Boston talk and the series of articles, by Victoria Gonda, goes into great detail about decompiling Kotlin
- “Exploring Kotlin’s hidden costs” series by Christophe B. is a great compilation of things to be aware of when working with Kotlin
Let’s now jump straight to the main topic: the constants.
Constants in Kotlin
Companion objects
There’s no static
keyword in Kotlin. If you want static access to certain fields or methods of
your class, you should put them under a companion object
. A naive way to declare a constant would
look like this:
class Constants {
companion object {
val FOO = "foo"
}
}
The field will be available globally and accessible through Constants.FOO
. But let’s now use the
decompiler and see how this code would look if written in Java (simplified variant):
public final class Constants {
@NotNull
private static final String FOO = "foo";
public static final Constants.Companion Companion = new Constants.Companion((DefaultConstructorMarker)null);
public static final class Companion {
@NotNull
public final String getFOO() {
return Constants.FOO;
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
It’s important to note here that a companion object is an actual object: Kotlin’s Constants.FOO
call will translate into Java’s Constants.Companion.getFOO()
. This version is pretty bad, as it
introduces an object and a method that could’ve been avoided.
const vals
A simple way to improve our example is to mark FOO
as const
:
class Constants {
companion object {
const val FOO = "foo"
}
}
Here’s the Java version:
public final class Constants {
@NotNull
public static final String FOO = "foo";
public static final Constants.Companion Companion = new Constants.Companion((DefaultConstructorMarker)null);
public static final class Companion {
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
The getter is gone, and we actually have direct static access to the field now, but we still have a
useless companion object generated by the compiler. Another thing to note about const
is that it
only works with primitives and Strings:
class Constants {
companion object {
// won't compile
const val FOO = Foo()
}
}
A workaround is to use the @JvmField
annotation on the val
:
class Constants {
companion object {
@JvmField val FOO = Foo()
}
}
This will make the Foo
instance public static final
. There’s an important difference between the
behavior of const
and @JvmField
: accesses to a const val
get inlined by the compiler, while
@JvmField
s don’t. Let’s take the following code:
fun main(args: Array<String>) {
println(Constants.FOO)
}
Here’s what we get with @JvmField val FOO = Foo()
:
public final class MainKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
Foo var1 = Constants.FOO;
System.out.println(var1);
}
}
and with const val FOO = "foo"
:
public final class MainKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
String var1 = "foo";
System.out.println(var1);
}
}
There’s no call to Constants.FOO
in the second example, the value has been inlined.
Dropping the class and the object
If all we need is just a set of constants - we can safely drop both the class and the object and use
top-level val
s:
const val FOO = "foo"
The result is exactly as you would’ve written it in Java:
public final class ConstantsKt {
@NotNull
public static final String FOO = "foo";
}
In Kotlin, you’ll be accessing the value by its name globally. If you’re using the value in Java
code, you’ll call ConstantsKt.FOO
. To avoid Kt
suffixes on class names, use the file:@JvmName
annotation on top of the file to specify a more readable name:
@file:JvmName("Constants")
Compiler will use provided value for the class name:
public final class Constants {
@NotNull
public static final String FOO = "foo";
}
Conclusion
Even though there’s no static
keyword in Kotlin, it’s easy to define constants that are globally
accessible. It’s also quite easy to get this wrong and introduce redundant methods and object
allocations into the bytecode. The decompiler tool can help you locate and fix this kind of issues,
and comes with an additional benefit - you’ll quickly learn how all that Kotlin magic works under
the hood! Have fun!