Java Interop Workbook
Ten short exercises on Kotlin/Java interop — getters as properties, platform types, SAM conversion, and the @Jvm annotations that make Kotlin pleasant to call from Java.
Practice problems for Calling Java From Kotlin (and Back) Without Friction. Each takes a minute or two. Each exercise ships with a runnable editor — fill in the TODO, press Run to compile it on JetBrains’ Kotlin server (the JDK is right there, so java.* calls work), then click Show answer to check yourself. Nothing here is a trick question, just direct practice of the syntax from the lesson.
calling Java from Kotlin
1. A getter is a property
A Java Person has getName() and setAge(int). Read the name and set the age to 30, Kotlin-style.
fun main() {
// Thread is a Java class with getName()/setPriority(int)
val t = Thread()
// TODO: read the name as a property, and set the priority to 3 property-style
val name = t.getName()
t.setPriority(3)
println(name + " / " + t.getPriority())
} Show answer Hide answer
val name = person.name
person.age = 30Kotlin sees a Java getter/setter pair as a property.
2. Pin a platform type
person.getName() returns a platform type. Bind it to a Kotlin val that asserts non-null so a null fails fast here.
fun main() {
// System.getProperty returns a platform type (String!)
// TODO: bind it to a val that asserts non-null
val version = System.getProperty("java.version")
println(version)
} Show answer Hide answer
val name: String = person.name 3. SAM conversion
A Java executor.submit(Runnable) expects a Runnable. Submit a task that calls doWork() without writing an anonymous class.
import java.util.concurrent.Executors
fun main() {
val executor = Executors.newSingleThreadExecutor()
// TODO: submit the task as a lambda instead of an anonymous Runnable (SAM conversion)
executor.submit(object : Runnable {
override fun run() {
println("working")
}
})
executor.shutdown()
} Show answer Hide answer
executor.submit { doWork() }Kotlin converts the lambda to the single-method Java interface automatically.
calling Kotlin from Java
4. A real static
Expose a companion load() so Java can call Config.load() rather than Config.Companion.load().
class Config {
companion object {
// TODO: annotate load() so Java can call Config.load() (not Config.Companion.load())
fun load(): Config = Config()
}
}
fun main() {
println(Config.load())
} Show answer Hide answer
class Config {
companion object {
@JvmStatic fun load(): Config = Config()
}
} 5. Overloads for Java callers
Java can’t call a function and skip parameters. Make connect(host, port = 443) callable from Java both with and without the port.
// TODO: add @JvmOverloads so Java can call this with or without the port
fun connect(host: String, port: Int = 443) {
println("connecting to " + host + ":" + port)
}
fun main() {
connect("example.com")
connect("example.com", 8080)
} Show answer Hide answer
@JvmOverloads
fun connect(host: String, port: Int = 443) { /* ... */ }@JvmOverloads generates the overloads default arguments made unnecessary in Kotlin.
6. Declare a checked exception
A read() function can throw IOException, and you want Java’s compiler to see it. Add the annotation.
import java.io.IOException
// TODO: add @Throws(IOException::class) so Java's compiler sees the checked exception
fun read(): String {
return "data"
}
fun main() {
println(read())
} Show answer Hide answer
@Throws(IOException::class)
fun read(): String { /* ... */ }Needed precisely because Kotlin has no checked exceptions of its own.
7. Name the file class
Top-level functions in Strings.kt land in a StringsKt class for Java. Make Java call them on StringUtils instead.
// TODO: add @file:JvmName("StringUtils") at the very top, before anything else
fun shout(s: String) = s.uppercase()
fun main() {
println(shout("hi"))
} Show answer Hide answer
@file:JvmName("StringUtils")
package textThis annotation goes at the very top of the file, before package.
gotchas
8. Unit becomes void
Write a Kotlin log(msg: String) that returns no meaningful value, and note (as a comment) the signature Java sees.
fun log(msg: String) {
// TODO: print msg — a Unit return compiles to void in Java
}
fun main() {
log("hello")
println("done")
} Show answer Hide answer
fun log(msg: String) {
println(msg)
}
// Java sees: void log(String msg)A Kotlin Unit return compiles to void; conversely a Java void method is Unit in Kotlin.
9. Protect a list crossing into Java
A Kotlin read-only List is still mutable from Java (it’s just java.util.List at runtime). Write a function that hands Java a copy nothing can mutate behind your back.
fun main() {
// TODO: return a copy a Java caller cannot mutate behind your back (use toList)
fun snapshot(items: List<String>): List<String> = items
println(snapshot(listOf("a", "b")))
} Show answer Hide answer
fun snapshot(items: List<String>): List<String> = items.toList()toList() returns a fresh list, so a Java caller’s .add() can’t disturb the original.
10. A plain field
Expose a Kotlin property to Java as a public field with no getter/setter pair.
class Point {
// TODO: expose x to Java as a plain public field (no getter/setter) with @JvmField
val x: Int = 0
}
fun main() {
println(Point().x)
} Show answer Hide answer
class Point {
@JvmField val x: Int = 0
} Going deeper: platform types and the @Jvm hints
12. Pin down a platform type
A Java method getName() returns a String with unknown nullability, so Kotlin sees it as a platform type String!. You want to fail fast at the boundary if it’s ever null. How do you declare the receiving variable, and what’s the alternative if null is actually valid? Reveal to check.
Show answer Hide answer
Assert non-null by annotating the type: val name: String = javaApi.getName() — this throws right at the boundary if it’s null, which is where you want the failure. If null is legitimate, declare val name: String? = javaApi.getName() and handle it. Best of all, annotate the Java side (@NonNull/@Nullable or JSpecify @NullMarked), which makes Kotlin enforce the real nullability and the platform type disappears.
13. Match the annotation to the problem
For each, name the @Jvm annotation a Java caller needs: (a) calling a Kotlin function that has default arguments, (b) calling a companion-object function as a plain static, (c) catching a checked exception the Kotlin function throws. Reveal to check.
Show answer Hide answer
(a) @JvmOverloads — generates the overloads Java needs since it can’t skip parameters. (b) @JvmStatic — emits a real static instead of forcing Companion.foo(). (c) @Throws(...) — writes the throws metadata Java’s compiler wants. (And @JvmName renames a function or file class; @JvmField exposes a property as a plain field.)
14. The guarantee that doesn’t cross over
You return a Kotlin read-only List<Int> from a function, and Java code calls .add(99) on it. Does it work? Reveal to check.
Show answer Hide answer
Yes — Kotlin’s read-only List is java.util.List on the JVM, so .add() is right there for Java to call. Read-only is a compile-time distinction that evaporates crossing into Java (just as internal becomes mangled-public). If immutability must survive the trip, hand Java a genuine copy.
Back to the lesson, Calling Java From Kotlin (and Back), or on to the next one: the scope functions.
Comments