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:

  1. const can only be used with values known at compile-time. (Alas, poor Logger, we can’t know you early enough 😿)
  2. Logger isn’t a primitive/String, and const 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: