Most Kotlin code runs alongside Java libraries and frameworks; smooth interop is a key Kotlin strength.
Calling Java from Kotlin mostly just works:
- Java getters and setters appear as properties:
user.getName()/setName()becomeuser.name. - Java types have platform types (
String!): Kotlin doesn't know their nullability, so it lets you treat them as nullable or not, and a wrong guess fails at runtime. Declare the Kotlin type explicitly at the boundary (val name: String? = javaObj.name). Nullability annotations (@Nullable, JSpecify@NullMarked) on the Java side give Kotlin real types. - SAM conversion: a lambda can be passed where a Java single-abstract-method interface (
Runnable,Comparator) is expected. For Kotlin interfaces, declare themfun interfaceto get the same. - Java's checked exceptions don't need to be caught.
- Escape Kotlin keywords used as Java names with backticks: `
Mockito.when(...)`.
Calling Kotlin from Java needs a few annotations:
- Top-level functions in
Utils.ktare static methods ofUtilsKt; rename with@file:JvmName("Utils"). @JvmStaticon companion or object members creates real static methods;@JvmFieldexposes a property as a public field.@JvmOverloadsgenerates overloads for default arguments, which Java can't use otherwise.@Throws(IOException::class)declares checked exceptions for Java callers.
Kotlin collections map to Java's (List is java.util.List); read-only is a compile-time view only, so Java code can still mutate them.
Example
import java.util.Optional
import java.util.concurrent.Executors
fun interface Validator { fun validate(s: String): Boolean } // SAM for Kotlin
object MathUtils {
@JvmStatic fun twice(x: Int) = x * 2 // static for Java
@JvmOverloads fun greet(name: String, greeting: String = "Hi") = "$greeting $name"
}
fun main() {
val sb = java.lang.StringBuilder()
sb.append("abc")
println(sb.length) // 3 (Java length() as a property)
val names = java.util.ArrayList<String>()
names.add("b"); names.add("a")
names.sortWith { x, y -> x.compareTo(y) } // lambda -> java.util.Comparator
println(names) // [a, b]
val env: String? = System.getenv("NOT_SET") // platform type made explicit
println(env ?: "unset") // unset
val notBlank = Validator { it.isNotBlank() }
println(notBlank.validate(" ")) // false
println(Optional.ofNullable<String>(null).orElse("none")) // none
val pool = Executors.newSingleThreadExecutor()
println(pool.submit<Int> { MathUtils.twice(21) }.get()) // 42
pool.shutdown()
}Remember
- Java getters/setters become Kotlin properties
- Platform types (T!) have unknown nullability: declare the type at the boundary
- Lambdas convert to Java SAM interfaces; use fun interface in Kotlin
- @JvmStatic, @JvmOverloads, @JvmName shape the Java-facing API
Watch out for
- Letting a Java method's platform-typed null flow into a non-null Kotlin variable, causing an NPE far from the source.