Kotlin

Null safety in Kotlin

Kotlin is a statically typed JVM language developed by Jetbrains. It has some good documentation so today I will focus on a tiny part of it – null safety.

Gjersjøen lake

There are at least couple of approaches to null handling in JVM languages:

  • Java doesn’t go much further than C – every reference (“pointer”) can be null, whether you like it or not. If it’s not a primitive, every single field, parameter or return value can be null.
  • Groovy has similar background but adds some syntactic sugar, namely Elvis Operator (?:) and Safe Navigation Operator (?.).
  • Clojure renames null to nil, additionally treating it as false in boolean expressions. NullPointerException is still possible.
  • Scala is first to adopt systematic, type safe Option[T] monad (Java 8 will have Optional<T> as well!) Idiomatic Scala code should not contain nulls but when interoperating with Java you must sometimes wrap nullable values.

Kotlin takes yet another approach. References that can be null have different type, thus null-safety is encoded in the type system and enforced only during compilation. We get NullPointerException-free code and no runtime overhead due to extra Option wrapper.

In the syntax layer each type T has a super type T? that allows null. Have a look at these trivial examples:

fun hello(name: String) {
    println("Hello, ${name}")
}
 
fun main(args: Array<String>) {
    val str = "Kotlin"
    hello(str)
 
    val maybeStr: String? = "Maybe?"
    hello(maybeStr)     //doesn't COMPILE
 
    if(maybeStr != null) {
        hello(maybeStr)
    }
 
}

Type of str is inferred to String. Function hello() accepts String so hello(str) is fine. However we explicitly declare maybeStr as String? type (nullable String). The compiler prevents us from calling hello() with String? due to incompatible type.

However if the compiler can prove that a call is safe, e.g. because we just checked for null, compilation succeeds. To be precise, the compiler can prove that downcasting from String? to String is safe. Similarly I always found it annoying in Java that after using instanceof operator (being annoying on its own) I still have to down cast my object:

Object obj = "Any object"
 
if(obj instanceof String) {
    hello((String)obj)
}

Not in Kotlin:

val obj: Any = "Any object"
 
if(obj is String) {
    hello(obj)
}

See? obj is of type Any (Object in Java terms) so calling hello(obj) is doomed to fail, right? Not quite. The compiler can prove that obj is actually of type String so it performs automatic, safe downcasting for us. Neat! But back to null handling.

I said a lot about downcasting, remembering that any non-null type T has a super type of nullable T?. Just like in any other polymorphic language upcasting is implicit. In other words we can pass type T when T? is required – which is quite obvious:

val str: String = "Hello"     //String type can be inferred here
unsafeHello(str)
 
fun unsafeHello(name: String?) {
 
}

Interestingly primitives can also be nullable:

fun safePositive(x: Int) = x > 0
fun unsafePositive(x: Int?): Boolean = x != null && x > 0

In generated bytecode former method takes int while the latter java.lang.Integer. While we are at it, first two expressions compile, but not the last one:

if(unsafePositive(maybeInt)) {
    //...
}
 
if(maybeInt != null && safePositive(maybeInt)) {
    //...
}
 
if(safePositive(maybeInt)) {
    //...
}

First expression has a perfect type match (Int? vs. Int?). In the second case the compiler can prove that maybeInt can be downcasted to Int, required by safePositive(). This can’t be proven in the last case, resulting in type mismatch compilation error.

So far it looks great – null safety with no extra runtime overhead. However Java interoperability is Achilles’ heel of Kotlin. In Scala Option[T] wrapper is implemented on top of the language and Scala itself allows null for Java interop. You won’t see null in idiomatic Scala code, but it pops up sometimes when interacting with Java collections and libraries. Typically extra Option(javaMethod()) delegation is required.

However Kotlin takes much more aggressive approach: every parameter of every Java method is considered nullable (that we don’t care), but also every return value is nullable – unless stated otherwise. It turns out that Kotlin compiler has some knowledge of JDK:

val formatted: String = String.format("Kotlin-is-%s", "cool")
val joined:    String = String.join("-", "Kotlin", "is", "cool")

First line compiles just fine, Kotlin knows that String.format() never returns null. However it can’t say that about String.join(), new in Java 8. Thus, even though String.join() never returns null as well, you still get String? inferred type. The same applies to any library or your custom Java code. Unfortunately @javax.validation.constraints.NotNull annotation doesn’t help, not to mention you can’t add annotations to library/JDK code.

Well.. you sort of can… IntelliJ IDEA has an obscure feature called External Annotations which lets you annotate arbitrary method, even in external JARs. You cannot change external code so such annotations are kept in special annotations.xml file:

<root>
    <item name='java.lang.String java.lang.String join(java.lang.CharSequence, java.lang.CharSequence...)'>
        <annotation name='org.jetbrains.annotations.NotNull'/>
    </item>
</root>

This declaration (of course IntelliJ manages it for you) tells Kotlin compiler that String.join() can’t return null. Because our code won’t compile without it, it must be checked into version control and becomes part of your code base.

Doesn’t seem like this problem would go away soon. There will always be libraries without @NotNull annotations and the compiler can’t possibly detect whether Java method is nullable or not (especially taking dynamic nature of class loading and CLASSPATH). More portable solution is to simply force down casting to null-safe type:

String.join("-", "Kotlin", "is", "cool") as String

…but it feels superfluous.

To wrap things up: null handling in Kotlin is both radical (type-safety and compile-time checking) and conservative ( null is still here, no functional, monadic style). I only hope that rough edges in Java interoperability will eventually go away.
 

Reference: Null safety in Kotlin from our JCG partner Tomasz Nurkiewicz at the Java and neighbourhood blog.

Tomasz Nurkiewicz

Java EE developer, Scala enthusiast. Enjoying data analysis and visualization. Strongly believes in the power of testing and automation.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button