Skip to Content
Course content

214: Built-In Annotations: @JvmStatic, @JvmOverloads, @JvmName

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you're writing a pure Kotlin project, you can mostly ignore the annotations we're covering today. But in the real world, you're almost always dropping Kotlin into a project that already has a mountain of legacy Java code. The problem is that Kotlin does some clever things under the hood—like companion objects and default parameters—that don't map one-to-one to Java bytecode. This results in Java code that looks clunky or even fails to compile.

I've spent a lot of time writing "bridge" libraries where Kotlin provides the logic and Java consumes it. Let's build a small NetworkUtils helper to see where these friction points are and how to smooth them over.

Cleaning up the "Companion" clutter

Let's start with a basic utility object. I want a way to initialize a network session that Java developers can call easily.

object NetworkUtils {
    fun initialize() {
        println("Network initialized")
    }
}

Now, if I try to call this from a Java class, I hit my first snag. I expected to write NetworkUtils.initialize(), but the IDE tells me that doesn't exist. Instead, I have to write:

NetworkUtils.Companion.initialize();

That .Companion part is a leak of Kotlin's internals into Java. It's ugly and it's not how Java developers expect static utilities to work. To fix this, I'll use @JvmStatic. This tells the Kotlin compiler to actually generate a real static method in the class file.

object NetworkUtils {
    @JvmStatic
    fun initialize() {
        println("Network initialized")
    }
}

Now, the Java side is clean: NetworkUtils.initialize(). Much better.

Making default parameters visible to Java

One of my favorite Kotlin features is default arguments. I can create a connect method that defaults the timeout to 30 seconds so the caller doesn't have to specify it every time.

object NetworkUtils {
    // ... initialize method ...

    fun connect(url: String, timeout: Int = 30) {
        println("Connecting to $url with timeout $timeout")
    }
}

Here is where I usually trip up if I'm rushing. I'll jump over to my Java file and try to call NetworkUtils.connect("https://api.example.com"). It fails. Java doesn't understand Kotlin's default parameters; it only sees one method that requires both arguments.

To solve this, I'll add @JvmOverloads. This tells Kotlin to generate multiple overloaded versions of the method in the bytecode—one with all arguments, and one for every subset of arguments that have defaults.

object NetworkUtils {
    @JvmStatic
    @JvmOverloads
    fun connect(url: String, timeout: Int = 30) {
        println("Connecting to $url with timeout $timeout")
    }
}

Now Java sees two methods: connect(String url) and connect(String url, int timeout). The Java developer gets the convenience of the default value without even knowing Kotlin is doing the heavy lifting.

Resolving name collisions with @JvmName

Finally, let's look at a weird edge case. Suppose I have a property and a function that, when compiled to JVM bytecode, end up with the exact same name. This happens often with "getters."

object NetworkUtils {
    // ... other methods ...

    val connectionStatus: String = "Online"

    fun getConnectionStatus() {
        println("Checking status...")
    }
}

Kotlin is fine with this. But wait—Kotlin automatically generates a getter method for the connectionStatus property called getConnectionStatus(). Now we have two methods with the exact same signature in the bytecode. The compiler will throw an error because it doesn't know which one to use when Java calls it.

I can't really rename the property without breaking my Kotlin logic, so I'll use @JvmName to change how the function appears to Java only.

object NetworkUtils {
    val connectionStatus: String = "Online"

    @JvmStatic
    @JvmName("logConnectionStatus")
    fun getConnectionStatus() {
        println("Checking status...")
    }
}

In my Kotlin code, I still call NetworkUtils.getConnectionStatus(). But in Java, I now call NetworkUtils.logConnectionStatus(). We've successfully avoided the collision while keeping the Kotlin API clean.




📋 Practical Task

Exercise: Creating a Java-Friendly Database Migration Helper

You are tasked with creating a DbMigrationManager singleton in Kotlin that will be used by a legacy Java backend. The Java team has requested a specific API surface to make their migration scripts cleaner.

Implement the following in a Kotlin object DbMigrationManager:

  • A method migrate() that is callable as a static method in Java (DbMigrationManager.migrate()).
  • A method runMigration(version: Int, force: Boolean = false). Use an annotation so that Java users can call it as runMigration(5) without providing the boolean.
  • A read-only property called currentVersion.
  • A function also called currentVersion() that prints the current version to the console. Use @JvmName to ensure the Java side calls this function as printCurrentVersion() to avoid a clash with the property's getter.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.