-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
214: Built-In Annotations: @JvmStatic, @JvmOverloads, @JvmName
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 asrunMigration(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@JvmNameto ensure the Java side calls this function asprintCurrentVersion()to avoid a clash with the property's getter.
There are no comments for now.