article banner

Effective Kotlin Item 3: Eliminate platform types as soon as possible

This is a chapter from the book Effective Kotlin. You can find it on LeanPub or Amazon.

The null safety introduced by Kotlin is amazing. Java was known in the community for Null Pointer Exceptions (NPE), which Kotlin’s safety mechanisms make rare or eliminate entirely. However, one thing that cannot be secured completely is a connection between Kotlin and a language that does not have solid null safety, such as Java or C. Imagine that we use a Java method that declares String as a return type. What type should it be in Kotlin?

If it is annotated with the @Nullable annotation, then we assume it is nullable and we interpret it as String?. If it is annotated with @NotNull, then we trust this annotation and we type it as String. However, what if this return type is not annotated with either of these annotations?

// Java
public class JavaTest {

    public String giveName() {
        // ...
    }
}

We might say that we should treat such a type as nullable. This would be a safe approach since everything is nullable in Java. However, we often know that something is not null, so we would end up using the non-null assertion !! in many places all around our code.

The real problem would arise when we need to take generic types from Java. Imagine that a Java API returns a List<User> that is not annotated at all. If Kotlin assumed nullable types by default and we did know that this list and those users are not null, we would need to not only assert the whole list but also filter the nulls:

// Java
public class UserRepo {

    public List<User> getUsers() {
        //***
    }
}
// Kotlin val users: List<User> = UserRepo().users!!.filterNotNull()

What if a function returned a List<List<User>> instead? This gets complicated:

val users: List<List<User>> = UserRepo().groupedUsers!! .map { it!!.filterNotNull() }

Lists at least have functions like map and filterNotNull. In other generic types, nullability would be an even bigger problem. This is why instead of being treated as nullable by default, a type that comes from Java and has unknown nullability is a special type in Kotlin: it is called a platform type.

C> Platform type - 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!. Platform types are non-denotable, meaning that one cannot write them explicitly in code. When a value with a platform type is assigned to a Kotlin variable or property, its type can be inferred, but it cannot be explicitly specified. Instead, we can specify either a nullable or a non-nullable type.

// Java
public class UserRepo {
    public User getUser() {
        //...
    }
}
// Kotlin val repo = UserRepo() val user1 = repo.user // Type of user1 is User! val user2: User = repo.user // Type of user2 is User val user3: User? = repo.user // Type of user3 is User?

Thanks to this, getting generic types from Java is not problematic:

val users: List<User> = UserRepo().users val users: List<List<User>> = UserRepo().groupedUsers

Casting platform types to non-nullable types is better than not specifying a type at all, but it is still dangerous because something we assumed to be non-null might be null. This is why, for safety reasons, I always suggest being very careful when we get platform types from Java. Remember that even if a function does not return null now, that doesn’t mean that it won’t change in the future. If its designer hasn’t specified it with an annotation or by describing it in a comment, this behavior can still be introduced without changing a contract.

If you have some control over Java code that needs to interoperate with Kotlin, introduce @Nullable and @NotNull annotations wherever possible.

// Java

import org.jetbrains.annotations.NotNull;

public class UserRepo {
    public @NotNull User getUser() {
        //...
    }
}

This is one of the most important steps when we want to support Kotlin developers well (and it's also important information for Java developers). Annotating many exposed types was one of the most important changes that were introduced in the Android API after Kotlin became a first-class citizen. This made the Android API much more Kotlin-friendly.

Note that many different kinds of annotations are supported, including those by:

  • JetBrains (@Nullable and @NotNull from org.jetbrains.annotations).
  • Android (@Nullable and @NonNull from androidx.annotation as well as from com.android.annotations and from the android.support.annotations) support library.
  • JSR-305 (@Nullable, @CheckForNull and @Nonnull from javax.annotation).
  • JavaX (@Nullable, @CheckForNull, @Nonnull from javax.annotation).
  • FindBugs (@Nullable, @CheckForNull, @PossiblyNull and @NonNull from edu.umd.cs.findbugs.annotations).
  • ReactiveX (@Nullable and @NonNull from io.reactivex.annotations).
  • Eclipse (@Nullable and @NonNull from org.eclipse.jdt.annotation).
  • Lombok (@NonNull from lombok).

Alternatively, in Java you can specify that all types should be non-nullable by default using JSR 305’s @ParametersAreNonnullByDefault annotation.

There is something we can do in our Kotlin code as well. My recommendation is to eliminate these platform types as soon as possible for safety reasons. To understand why, think about the difference between how the statedType and platformType functions behave in this example:

// Java
public class JavaClass {
    public String getValue() {
        return null;
    }
}
// Kotlin fun statedType() { val value: String = JavaClass().value //... println(value.length) } fun platformType() { val value = JavaClass().value //... println(value.length) }

In both cases, the developer assumed that getValue would not return null, but this is wrong because it results in an NPE in both cases, but there’s a difference in where this error happens.

In statedType, the NPE will be thrown in the same line where we get the value from Java. It would be absolutely clear that we wrongly assumed a non-nullable type and we got null. We would just need to change it and adjust the rest of our code to this change.

In platformType, the NPE will be thrown when we use this value as non-nullable (possibly from the middle of a more complex expression). A variable typed as a platform type can be treated as both nullable and non-nullable. Such a variable might be used a few times safely, but then it gets used unsafely and throws an NPE. When we use such properties, the type system does not protect us. This is a similar situation as in Java, but in Kotlin we do not expect to cause an NPE just by using an object. It is very likely that sooner or later someone will unsafely use a variable type as a platform type, and then we will end up with a runtime exception whose cause might not be easy to find.

// Java
public class JavaClass {
    public String getValue() {
        return null;
    }
}
// Kotlin fun statedType() { val value: String = JavaClass().value // NPE //... println(value.length) } fun platformType() { val value = JavaClass().value //... println(value.length) // NPE }

Even more dangerously, a platform type might be propagated further. For instance, we might expose a platform type as a part of our interface:

interface UserRepo { fun getUserName() = JavaClass().value }

In this case, the method’s inferred type is a platform type. This means that anyone can still decide if it is nullable or not. One might choose to treat it as nullable in a definition site, and as a non-nullable in the use site:

class RepoImpl : UserRepo { override fun getUserName(): String? { return null } } fun main() { val repo: UserRepo = RepoImpl() val text: String = repo.getUserName() // NPE in runtime print("User name length is ${text.length}") }

Propagating a platform type is a recipe for disaster. They are problematic, so for safety reasons we should always eliminate them as soon as possible. In this case, IDEA IntelliJ helps us with a warning:

Summary

Types that come from another language and have unknown nullability are known as platform types. Since they are dangerous, we should eliminate them as soon as possible and not let them propagate. It is also good to specify types using annotations that specify nullability for exposed Java constructors, methods and fields. This is precious information for Java and Kotlin developers who use these elements.