Kotlin is derived from the JVM platform, so Kotlin/JVM is its most mature flavor, compared to, e.g., Kotlin/JS or Kotlin/Native. But Kotlin and other JVM languages, like Java, are still different programming languages, therefore some challenges are inevitable when trying to get these languages to cooperate. So, some extra effort might be needed to make them work together as smoothly as possible. Let's see some examples.
Nullable types
Java cannot mark that a type is not nullable as all its types are considered nullable (except for primitive types). In trying to correct this flaw, Java developers started using Nullable and NotNull annotations from a number of libraries that define such annotations. These annotations are helpful but do not offer the safety that Kotlin offers. Nevertheless, in order to respect this convention, Kotlin also marks its types using Nullable and NotNull annotations when compiled to JVM[^051_3].
class MessageSender {
fun sendMessage(title: String, content: String?) {}
}
// Compiles to the analog of the following Java codefinalclassMessageSender{publicvoidsendMessage(@NotNullString title,@NullableString content
){Intrinsics.checkNotNullParameter(title,"title");}}
On the other hand, when Kotlin sees Java types with Nullable and NotNull annotations, it treats these types accordingly as nullable and non-nullable types[^051_0].
This makes interoperability between Kotlin and Java automatic in most cases. The problem is that when a Java type is not annotated, Kotlin does not know if it should be considered nullable or not. One could assume that a nullable type should be used in such a case, but this approach does not work well. Just consider a Java function that returns Observable<List<User>>, which in Kotlin is seen as Observable<List<User?>?>?. There would be so many types to unpack, even though we know none of them should actually be nullable.
// Kotlin, if unannotated types were considered nullable
val repo = UserRepo()
val users: Observable<List<User>> = repo.fetchUsers()!!
.map { it!!.map { it!! } }
This is why Kotlin introduced the concept of platform type, which is a type that comes from another language and has unknown nullability. Platform types are notated with a single exclamation mark ! after the type name, such as String!, but this notation cannot be used in code. Platform types are non-denotable, meaning that they can’t be written explicitly in code. When a platform value is assigned to a Kotlin variable or property, it can be inferred, but it cannot be explicitly set. Instead, we can choose the type that we expect: either a nullable or a non-null type.
// Kotlin
val repo = UserRepo()
val user1 = repo.fetchUsers()
// The type of user1 is Observable<List<User!>!>!
val user2: Observable<List<User>> = repo.fetchUsers()
val user3: Observable<List<User?>?>? = repo.fetchUsers()
Casting platform types to non-nullable types is better than not specifying a type at all, but it is still dangerous because something we assume is non-null might be null. This is why, for safety reasons, I always suggest being very careful when we get platform types from Java[^051_1].
Kotlin type mapping
Nullability is not the only source of differences between Kotlin and Java types. Many basic Java types have Kotlin alternatives. For instance, java.lang.String in Java maps to kotlin.String in Kotlin. This means that when a Kotlin file is compiled to Java, kotlin.String becomes java.lang.String. This also means that java.lang.String in a Java file is treated like it is kotlin.String. You could say that kotlin.String is type aliased to java.lang.String. Here are a few other Kotlin types with associated Java types:
Type mapping is slightly more complicated for primitive types and types that represent collections, so let's discuss them.
JVM primitives
Java has two kinds of values: objects and primitives. In Kotlin, all values are objects, but the Byte, Short, Int, Long, Float, Double, Char, and Boolean types use primitives under the hood. So, for example, when you use Int as a parameter, it will be int under the hood.
// KotlinFile.kt
fun multiply(a: Int, b: Int) = a * b
// Compiles to the analog of the following Java codepublicfinalclassKotlinFileKt{publicstaticfinalintmultiply(int a,int b){return a * b;}}
Kotlin uses primitives whenever possible. Here is a table of all types that are compiled into primitives.
| Kotlin type | Java type |
|-------------|-----------|
| Byte | byte |
| Short | short |
| Int | int |
| Long | long |
| Float | float |
| Double | double |
| Char | char |
| Boolean | boolean |
Primitives are not nullable on JVM, so nullable Kotlin types are always compiled into non-primitive types. For types that could be otherwise represented as primitives, we say these are wrapped types. Classes like Integer or Boolean are just simple wrappers over primitive values.
Only arrays can store primitives, so Kotlin introduced special types to represent arrays of primitives. For instance, to represent an array of primitive ints, we use IntArray, where Array<Int> represents an array of wrapped types.
| Kotlin type | Java type |
|--------------|-------------|
| Array<Int> | Integer[] |
| IntArray | int[] |
Similar array types are defined for all primitive Java types.
Kotlin introduced a distinction between read-only and mutable collection types, but this distinction is missing in Java. For instance, in Java we have the List interface, which includes methods that allow list modification. But in Java we also use immutable collections, and they implement the same interface. Their methods for collection modification, like add or remove, throw UnsupportedOperationException if they are called.
This is a clear violation of the Interface Segregation Principle, an important programming principle that states that no code should be forced to depend on methods it does not use. This is an essential Java flaw, but this is hard to change due to backward compatibility.
Kotlin introduced a distinction between read-only and mutable collection types, which works perfectly when we operate purely on Kotlin projects. The problem arises when we need to interoperate with Java. When Kotlin sees a non-Kotlin file with an element expecting or returning the List interface, it does not know if this list should be considered mutable or not, so the result type is (Mutable)List, which can be used as both List and MutableList.
On the other hand, when we use Kotlin code from Java, we have the opposite problem: Java does not distinguish between mutable and read-only lists at the interface level, so both these types are treated as List.
// KotlinFile.kt
fun readOnlyList(): List<Int> = listOf(1, 2, 3)
fun mutableList(): MutableList<Int> = mutableListOf(1, 2, 3)
// Compiles to analog of the following Java codepublicfinalclassKotlinFileKt{@NotNullpublicstaticfinalListreadOnlyList(){returnCollectionsKt.listOf(newInteger[]{1,2,3});}@NotNullpublicstaticfinalListmutableList(){returnCollectionsKt.mutableListOf(newInteger[]{1,2,3});}}
This interface has mutating methods like add or remove, so using them in Java might lead to exceptions.
This fact can be problematic. A read-only Kotlin list might be mutated in Java because the transition to Java down-casts it to mutable. This is a hack that breaks the Kotlin List type contract[^051_2].
By default, all objects that implement a Kotlin List interface on JVM implement Java List, so methods like add or remove are generated for them. The default implementations of these methods throw UnsupportedOperationException. The same can be said about Set and Map.
[^051_0]: Kotlin supports Nullable and NotNull annotations from a variety of libraries, including JSR-305, Eclipse, and Android.
[^051_1]: More about handling platform types in Effective Kotlin, Item 3: Eliminate platform types as soon as possible.
[^051_2]: See Effective Kotlin, Item 31: Respect abstraction contracts.
[^051_3]: Kotlin, when compiled to JVM, uses Nullable and NotNull annotations from the org.jetbrains.annotations package.
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.
Software architect with 15 years of experience, currently working on building infrastructure for AI. I think Kotlin is one of the best programming languages ever created.
Owen has been developing software since the mid 1990s and remembers the productivity of languages such as Clipper and Borland Delphi.
Since 2001, He moved to Web, Server based Java and the Open Source revolution.
With many years of commercial Java experience, He picked up on Kotlin in early 2015.
After taking detours into Clojure and Scala, like Goldilocks, He thinks Kotlin is just right and tastes the best.
Owen enthusiastically helps Kotlin developers continue to succeed.