Kotlin / Concurrency, interoperability, and integrated design / Lesson 16
Defend the Kotlin and Java boundary
Handle platform nullability, collection contracts, numeric representation, and Java-facing API shape explicitly.
On this page
Lesson goals
- Explain why Java platform types can weaken a local non-null assumption.
- Review the Java-visible API rather than assuming Kotlin syntax appears unchanged.
Interoperability preserves integration, not every type guarantee
Kotlin/JVM can call Java libraries and expose Kotlin declarations to Java. At that boundary, nullability information may be incomplete. A Java reference without recognized nullability annotations can appear as a platform type, letting Kotlin code choose nullable or non-null treatment but leaving a possible runtime failure. Establish a validated local representation near the boundary instead of spreading platform assumptions through business logic. An annotation helps tools communicate intent; it does not make an incorrect Java implementation physically unable to return null or mutate a collection unexpectedly.
Normalize uncertain nullability before domain use
The two-file example contains a fictional legacy Java class that deliberately returns null. Kotlin receives it into an explicitly nullable local, then chooses a presentation fallback. The Java and Kotlin sources must be compiled together by a compatible mixed-language build; neither block alone is a complete runnable application. Do not use !! merely because the Java documentation says a value should exist. If absence violates a business contract, return an explicit integration failure instead of silently treating the fallback as a valid domain identity.
public final class LegacyNames {
public static String fetchName() { return null; }
}
This class intentionally returns null and has no main. It is one part of a proposed mixed Java/Kotlin example and was not compiled.
fun main() {
val name: String? = LegacyNames.fetchName()
println(name ?: "missing name")
}
missing name
When built with the preceding Java class, expected output is missing name. This is a reasoned result; no mixed-language build was executed.
Review mutability and exceptions across the boundary
Java collection APIs and Kotlin read-only interfaces do not create a deep immutable guarantee when values cross languages. Java may hold another alias, insert unexpected nulls where annotations and generics do not protect the contract, or return an unmodifiable object that throws on a mutation attempt. Validate inputs and copy where the ownership contract requires a snapshot. Kotlin treats exceptions as unchecked, while Java callers may need an appropriate throws declaration through interop annotations for a chosen API. Decide the caller-facing failure contract rather than assuming the two languages enforce identical rules.
Use exact Java value types where their semantics fit
Kotlin/JVM can use java.math.BigDecimal for decimal arithmetic. Constructing from a decimal string preserves that written decimal value, while constructing from a Double can preserve the Doubleโs binary approximation instead. The example adds two exact string-constructed values and prints a plain decimal form. A real financial model still needs currency, scale, rounding mode, overflow or range policy, and audit requirements. BigDecimal is a tool, not a complete money domain. Equality and numeric comparison can also differ in their treatment of scale, so choose the comparison that matches the business meaning.
import java.math.BigDecimal
fun main() {
val first = BigDecimal("0.10")
val second = BigDecimal("0.20")
val total = first.add(second)
println(total.toPlainString())
println(total.compareTo(BigDecimal("0.3")) == 0)
}
0.30
true
Expected output is 0.30 then true. compareTo checks numeric ordering here despite different written scales. The program requires the JVM platform and was not compiled or executed.
Inspect what a Java caller actually sees
Kotlin properties commonly appear as getters and setters to Java, and top-level functions are compiled into a generated file facade class unless configured otherwise. Default arguments do not automatically provide every Java overload, and companion functions are not automatically ordinary static methods. Annotations such as JvmName, JvmStatic, JvmOverloads, and Throws address particular interop needs with specific rules. Use them to support a declared caller contract, not as a blanket decoration. Review generated API signatures and mixed-language tests when tooling exists; this course performed no bytecode inspection or interop execution.
Common errors and fixes
A Java-returned value crashes at a non-null Kotlin access.
- Why it happens
- A platform type was accepted as non-null without validation.
- How to fix it
- Normalize nullability near the integration boundary.
- Prevent it next time
- Test absent and malformed Java-returned data explicitly.
Decimal output exposes an unexpected binary approximation.
- Why it happens
- A decimal value was created through an inexact Double input.
- How to fix it
- Use a suitable exact decimal input and explicit rounding policy.
- Prevent it next time
- Keep money representation independent from display formatting.
A Java caller cannot use the expected default-argument overload.
- Why it happens
- Kotlin source convenience was assumed to define Java API shape.
- How to fix it
- Review generated signatures and add intentional overload support if needed.
- Prevent it next time
- Include representative Java consumers in API review.
Check your understanding
1. Should an unexpected null from a required Java identifier become an empty string?
Show worked solution
Only if that is the explicit domain policy, which is uncommon for a required identity. An empty string can conceal the integration failure and later target the wrong path. Prefer a validated failure or structured result at the adapter, preserving safe diagnostic context without exposing sensitive data.
2. Does using BigDecimal alone guarantee correct financial behavior?
Show worked solution
No. It provides decimal arithmetic with defined operations, but the application must still specify currency, allowed scale, rounding, validation, and transaction semantics. The example proves none of those business policies, and its output is reasoned rather than observed because no Kotlin compiler was available.
Key takeaways
- Interop adapters should turn uncertain platform data into validated local types.
- Java-visible signatures and numeric semantics need explicit review.