article banner

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.

class MessageSender { fun sendMessage(title: String, content: String?) {} }
// 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() {
        //***
    }
}
// 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 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 typeJava type
kotlin.Anyjava.lang.Object
kotlin.Cloneablejava.lang.Cloneable
kotlin.Comparablejava.lang.Comparable
kotlin.Enumjava.lang.Enum
kotlin.Annotationjava.lang.Annotation
kotlin.Deprecatedjava.lang.Deprecated
kotlin.CharSequencejava.lang.CharSequence
kotlin.Stringjava.lang.String
kotlin.Numberjava.lang.Number
kotlin.Throwablejava.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.

// KotlinFile.kt fun multiply(a: Int, b: Int) = a * b
// 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 typeJava type
Bytebyte
Shortshort
Intint
Longlong
Floatfloat
Doubledouble
Charchar
Booleanboolean

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 typeJava 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 typeJava 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 typeJava type
Array<Int>Integer[]
IntArrayint[]

Similar array types are defined for all primitive Java types.

Kotlin typeJava type
ByteArraybyte[]
ShortArrayshort[]
IntArrayint[]
LongArraylong[]
FloatArrayfloat[]
DoubleArraydouble[]
CharArraychar[]
BooleanArrayboolean[]

Using these types as an array type argument makes an array or arrays.

Kotlin typeJava 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.

// 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 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.

0:

Kotlin supports Nullable and NotNull annotations from a variety of libraries, including JSR-305, Eclipse, and Android.

1:

More about handling platform types in Effective Kotlin, Item 3: Eliminate platform types as soon as possible.

2:

See Effective Kotlin, Item 31: Respect abstraction contracts.

3:

Kotlin, when compiled to JVM, uses Nullable and NotNull annotations from the org.jetbrains.annotations package.