Null safety in Kotlin
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 benull
. - Groovy has similar background but adds some syntactic sugar, namely Elvis Operator (
?:
) and Safe Navigation Operator (?.
). - Clojure renames
null
tonil
, additionally treating it asfalse
in boolean expressions.NullPointerException
is still possible. - Scala is first to adopt systematic, type safe
Option[T]
monad (Java 8 will haveOptional<T>
as well!) Idiomatic Scala code should not containnull
s but when interoperating with Java you must sometimes wrap nullable values.
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) {Type of
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)
}
}
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"Not in Kotlin:
if(obj instanceof String) {
hello((String)obj)
}
val obj: Any = "Any object"See?
if(obj is String) {
hello(obj)
}
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 hereInterestingly primitives can also be nullable:
unsafeHello(str)
fun unsafeHello(name: String?) {
}
fun safePositive(x: Int) = x > 0In generated bytecode former method takes
fun unsafePositive(x: Int?): Boolean = x != null && x > 0
int
while the latter java.lang.Integer
. While we are at it, first two expressions compile, but not the last one:if(unsafePositive(maybeInt)) {First expression has a perfect type match (
//...
}
if(maybeInt != null && safePositive(maybeInt)) {
//...
}
if(safePositive(maybeInt)) {
//...
}
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")First line compiles just fine, Kotlin knows that
val joined: String = String.join("-", "Kotlin", "is", "cool")
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>This declaration (of course IntelliJ manages it for you) tells Kotlin compiler that
<item name='java.lang.String java.lang.String join(java.lang.CharSequence, java.lang.CharSequence...)'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
</root>
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.Tags: kotlin, scala