Disclosure: AI-assisted content — consult with an organic-developer.
When building low-level libraries for the JVM — especially those that interact with JNI, rendering engines, or MethodHandles — the exact bytecode emitted matters. Recently I hit a limitation in Kotlin that reminded me why the JVM world still needs Java for certain things.
Where Kotlin Emits Different Bytecode than Java
Here are the main areas where Kotlin’s generated bytecode diverges from Java’s, and why that matters.
| Area | Java | Kotlin | Takeaway |
|---|---|---|---|
| Signature-polymorphic calls | Emits correct signature for MethodHandle.invokeExact | Falls back to (Object[])Object, causing mismatches | Keep these calls in Java |
| Default parameters | No defaults → use overloads | Generates synthetic $default methods with bitmask | Avoid defaults in public APIs for Java clients |
| Companion objects / @JvmStatic | True static methods | Methods live in $Companion unless annotated | Use @JvmStatic or plain Java for static APIs |
| Internal visibility | Package-private supported | internal compiles to public + metadata | Don’t rely on internal for cross-language encapsulation |
| SAM interfaces | Any functional interface = lambda | Only fun interface supports SAM; lambdas may create synthetic classes | Define callbacks in Java for performance |
| Nullability | All references nullable | Annotations encode nullability, JVM doesn’t enforce | Explicit null checks needed in low-level code |
| Suspend functions / coroutines | N/A | Compiles to (Arg, Continuation) → Object | Keep coroutines in Kotlin wrappers, not core API |
Other Kotlin Caveats for Low-Level Code
This wasn’t an isolated issue. Kotlin differs from Java in other ways that make it risky for core interop code:
| Area | Java | Kotlin limitation |
|---|---|---|
| JNI declarations | static native boolean render(int, int) | Needs @JvmStatic in a companion object; generates synthetic names |
| JNI header generation | javac -h works directly | No header generation for Kotlin sources |
| Checked exceptions | Enforced at compile-time | Kotlin ignores them (all unchecked) |
| Raw types | Allowed (List) | Always requires generics (List<*>) |
| Wildcards | ? super, ? extends supported | Only in / out; cannot express everything |
| Default params | Not supported (overloads instead) | Compiles to synthetic $default methods |
| Static members | static keyword | Requires @JvmStatic in object/companion |
| Suspend functions | N/A | Compiled to Continuation-based state machines, awkward for Java callers |
Why This Matters for Library Code
A low-level library often deals with:
- JNI ↔ JVM bridges
- OpenGL or native rendering loops
- Performance-critical calls that must inline
- Reflection and
MethodHandles
All of these require predictable bytecode and signatures. Kotlin often inserts synthetic classes ($Companion, $DefaultImpls, $WhenMappings) or adapts signatures in ways Java clients (and JNI) do not expect.
Why Keeping the Library Core in Java Makes Sense
| Benefit | Why It Matters |
|---|---|
| One language to maintain | Single codebase, easier contributor onboarding, faster builds |
| Interop for everyone | Java APIs work in all JVM languages; Kotlin clients lose nothing; Java clients stay safe from Kotlin-only features |
| JNI friendliness | Direct mapping of Java types to JNI (int → jint, boolean → jboolean); javac -h header generation works; avoids $Companion/$DefaultImpls surprises |
| Bytecode predictability | No synthetic baggage ($Companion, $default, $WhenMappings); avoids mismatched signatures; JIT optimizes exactly as written |
Strategy: Java Core + Optional Kotlin API
The pattern I adopted (and which many frameworks use):
- Core in Java
- Predictable bytecode
- JNI header generation
- Works with
MethodHandle,VarHandle,Unsafe - Safe for both Java and Kotlin clients
- Optional Kotlin extensions (
-ktx)- Extension functions for ergonomics
- Coroutines (
suspendwrappers) - Null-safety
- DSLs for configuration
This is the same model Android Jetpack follows:androidx.core in Java, androidx.core-ktx in Kotlin.
Takeaways
| Point | Why |
|---|---|
| MethodHandle support | Java compiler emits exact signatures ((int,int)boolean), Kotlin falls back to (Object[])Object, causing runtime issues |
| Bytecode predictability | Java produces direct, predictable bytecode; Kotlin adds synthetic constructs and indirections |
| JNI compatibility | Java maps directly to JNI types and header generation; Kotlin introduces complications |
| Best practice | Keep the core in Java for stability and performance; add Kotlin wrappers for ergonomics (DSLs, coroutines, null-safety) |
interesting link: https://www.okoone.com/spark/technology-innovation/why-kotlin-swift-and-ruby-are-dropping-off-the-radar/
References
- Java SE Docs —
MethodHandleand signature-polymorphic methods
https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/MethodHandle.html - OpenJDK Wiki — Deconstructing MethodHandles
https://wiki.openjdk.org/display/HotSpot/Deconstructing%2BMethodHandles - Kotlin Docs — Functions and default arguments
https://kotlinlang.org/docs/functions.html - Medium — Kotlin Function Parameters and Default Values: Behind the Scenes
https://medium.com/@AlexanderObregon/kotlin-function-parameters-and-default-values-behind-the-scenes-6551fa515fa1 - Kotlin Forum — Kotlin bytecode on default parameters
https://discuss.kotlinlang.org/t/kotlin-bytecode-on-default-parameters/6159 - Kotlin Docs — Visibility modifiers (
internal, etc.)
https://www.digitalocean.com/community/tutorials/kotlin-visibility-modifiers-public-protected-internal-private - YouTrack — KT-14416: Support of @PolymorphicSignature in Kotlin compiler
https://youtrack.jetbrains.com/issue/KT-14416 - Baeldung — The @JvmStatic Annotation in Kotlin
https://www.baeldung.com/kotlin/jvmstatic-annotation - StackOverflow — How to call a static JNI function from Kotlin?
https://stackoverflow.com/questions/57117201/how-to-call-a-static-jni-function-from-kotlin - The golden age of Kotlin and its uncertain future https://shiftmag.dev/kotlin-vs-java-2392/










