Kotlin and Java interoperability: Types
This is a chapter from the book Advanced Kotlin. You can find it on LeanPub or Amazon.
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 JVM3.
// Compiles to the analog of the following Java code
final class MessageSender {
public void sendMessage(
@NotNull String title,
@Nullable String 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 types0.
public class JavaClass {
public static @NotNull String produceNonNullable() {
return "ABC";
}
public static @Nullable String produceNullable() {
return null;
}
}
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.
// Java
public class UserRepo {
public Observable<List<User>> fetchUsers() {
//***
}
}
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.
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 Java1.
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:
Kotlin type | Java type |
---|---|
kotlin.Any | java.lang.Object |
kotlin.Cloneable | java.lang.Cloneable |
kotlin.Comparable | java.lang.Comparable |
kotlin.Enum | java.lang.Enum |
kotlin.Annotation | java.lang.Annotation |
kotlin.Deprecated | java.lang.Deprecated |
kotlin.CharSequence | java.lang.CharSequence |
kotlin.String | java.lang.String |
kotlin.Number | java.lang.Number |
kotlin.Throwable | java.lang.Throwable |
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.
// Compiles to the analog of the following Java code
public final class KotlinFileKt {
public static final int multiply(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.
Kotlin type | Java type |
---|---|
Byte? | Byte |
Short? | Short |
Int? | Integer |
Long? | Long |
Float? | Float |
Double? | Double |
Char? | Char |
Boolean? | Boolean |
Primitives cannot be used as generic type arguments, so collections use wrapped types instead.
Kotlin type | Java type |
---|---|
List<Int> | List<Integer> |
Set<Long> | Set<Long> |
Map<Float, Double> | Map<Float, Double> |
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 type | Java type |
---|---|
ByteArray | byte[] |
ShortArray | short[] |
IntArray | int[] |
LongArray | long[] |
FloatArray | float[] |
DoubleArray | double[] |
CharArray | char[] |
BooleanArray | boolean[] |
Using these types as an array type argument makes an array or arrays.
Kotlin type | Java type |
---|---|
Array<IntArray> | int[][] |
Array<Array<LongArray>> | long[][][] |
Array<Array<Int>> | Integer[][] |
Array<Array<Array<Long>>> | Long[][][] |
Collection 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.
// Java
public final class JavaClass {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3);
numbers.add(4); // throws UnsupportedOperationException
}
}
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
.
// Compiles to analog of the following Java code
public final class KotlinFileKt {
@NotNull
public static final List readOnlyList() {
return CollectionsKt.listOf(new Integer[]{1, 2, 3});
}
@NotNull
public static final List mutableList() {
return CollectionsKt.mutableListOf(new Integer[]{1,2,3});
}
}
This interface has mutating methods like add
or remove
, so using them in Java might lead to exceptions.
// Java
public final class JavaClass {
public static void main(String[] args) {
List<Integer> integers = KotlinFileKt.readOnlyList();
integers.add(20); // UnsupportedOperationException
}
}
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 contract2.
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
.
Kotlin supports Nullable
and NotNull
annotations from a variety of libraries, including JSR-305, Eclipse, and Android.
More about handling platform types in Effective Kotlin, Item 3: Eliminate platform types as soon as possible.
See Effective Kotlin, Item 31: Respect abstraction contracts.
Kotlin, when compiled to JVM, uses Nullable
and NotNull
annotations from the org.jetbrains.annotations
package.