5 minutes
Static in Kotlin
ed. note: This blog post was written in June 2020, when Kotlin 1.3.72 was in production. It’s possible the Kotlin compiler has gotten smarter since then!
My current company uses Kotlin for the backend services, so naturally we use the most standard of JVM logging setups: SLF4J + Logback.
From my old-timey Java days, I know that the idiomatic way to set up an SLF4J logger is like so:
public class Direction {
private static final Logger logger = LoggerFactory.getLogger(Direction.class)
public void navigate() {
logger.info(...)
}
}
But it’s 2020, and we write Kotlin now 🎉 A few websites I found helpfully suggest the most basic-but-functional Kotlin version of this:
class Direction {
private val LOG = LoggerFactory.getLogger(Direction::class.java)
public fun navigate() {
LOG.info("println(TAG)")
}
}
But you’ll note that this doesn’t achieve the same result as our Java snippet; a glance at the generated Java reveals this:
public final class Direction {
private final Logger LOG = LoggerFactory.getLogger(Direction.class);
public final void navigate() {
this.LOG.info("println(TAG)");
}
}
Yikes! Each new instance of Direction
will spin up a new LOG
as well, which can get expensive, if we’re not careful.
This generalizes to a whole bunch of situations where we’d prefer to have one instance of something available to a class. In Java, we’d use static
to solve this problem.
Automatic Static
Naturally, Kotlin doesn’t have any static
keywords. Nevertheless, from our encyclopedic knowledge of Kotlin reserved keywords, we know about const
. If we combine that with a companion object
, can we get what we want?
Actually, that doesn’t help us with logging! According to the docs (and the compiler!), there are two reasons this won’t work:
const
can only be used with values known at compile-time. (Alas, poorLogger
, we can’t know you early enough 😿)Logger
isn’t a primitive/String, andconst
only works with primitives
That’s fine, what if we remove the const
- isn’t a companion object basically like a Java static final
?
class Direction {
companion object {
private val LOG = LoggerFactory.getLogger(Direction::class.java)
}
public void navigate() {
LOG.info("println(TAG)")
}
}
The Java we generate is roughly:
public final class Direction {
private static final Logger LOG = LoggerFactory.getLogger(Direction.class);
public static final Direction.Companion Companion = new Direction.Companion();
public final void navigate() {
LOG.info("println(TAG)");
}
public static final class Companion {
private Companion() {}
}
}
Hey! That’s not bad! Pretty efficient, and the extra Companion
class doesn’t really concern me much. On Android, R8 happily inline that (assuming you’ve enabled the -accessmodification
flag), and on other systems (e.g. backend), the JVM should inline that as well.
But I don’t get paid to write pretty generated Java code, I get paid to write Kotlin. It’s not a lot to type, but companion object
with all those newlines can wear on you!
Well, what about a top-level declaration? Putting our Logger
setup there will definitely make it static
. Let’s check it out!
private val LOG = LoggerFactory.getLogger(Direction::class.java)
class Direction {
fun navigate() {
LOG.info("println(TAG)")
}
}
Gives us:
public final class DirectionKt {
private static final Logger LOG = LoggerFactory.getLogger(Direction.class);
// $FF: synthetic method
public static final Logger access$getLOG$p() {
return LOG;
}
}
public final class Direction {
public final void navigate() {
MyClassKt.access$getLOG$p().info("println(TAG)");
}
}
That synthetic accessor makes sense, since you’re essentially calling another class’s private field.
For folks who got into Kotlin but never wrote much Java, this generated code might be a bit surprising. But don’t worry – the perf impact there is negligible; once again, R8 or JVM HotSpot will realistically inline that for you.
But is this really the end of the road? Can Kotlin really only be half as “space-efficient” as Java?
What’s in a name?
The problem here is that Kotlin seems to generate another DirectionKt
class to warehouse the static elements - akin to a companion object. But what if there was a way to direct Kotlin to “merge” the *Kt
class with the “real” class?
Perhaps @JvmName
could save us from multiple classes? More specifically, prepending @file:JvmName("Direction")
to our previous Kotlin snippet gives us:
… a compile error 💀
e: /Users/parth/.../Direction.kt: (1, 1): Duplicate JVM class name 'Direction' generated from:
package-fragment, Direction
(Fun fact: the IntelliJ IDEA 2020.2 EAP totally lets you put the @file:JvmName
thing without showing you any lint or warnings)
Darn.
Conclusions - how to be static
But which is better? companion
or top-level declaration?
From a perf/size perspective, you get the same bytecode generated for you, so they’re the same.
Bikesheddingly Personally, I prefer the top-level declaration. It’s out-of-the-way, it’s less to read, and least importantly, almost 20 fewer characters to type!
⚠️ There is one “gotcha” with a top-level declaration: You must mark the top-level val
as private
, or you could get into wacky situations where another class accidentally uses another class’s Logger
instance.
Have fun tracking down that logging bug! 🙀🐛
That being said, if you’ve already got a companion
for something, there’s literally no reason you shouldn’t put your LOG
setup in there as well!
…Except for testability, but that’s a blog post for another day.
Happy logging 🌲 (or whatever else you put in static members)!
References | Extra Reading | Sources
Many thanks to Zac Sweers for telling me “you need to revise this”, and Jesse Wilson for pointing out that non-private top-level declarations can cause problems.
Also:
- Egor Andreevich - https://blog.egorand.me/where-do-i-put-my-constants-in-kotlin/
- Christophe Beyls - https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62
- Roman Elizarov (source):
If you follow the style of writing the statics at the top of the class in Java, then I suggest to try to consider writing them at the top level (e.g. before class declaration) in Kotlin. For many use-cases it works better than introducing a
companion object.