Author: Pan Haozhe
“I call it my billion-dollar mistake. It was the invention of the null reference…My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement.”
-Tony Hoare
This document explains Kotlin's null safety feature. For an overview of Kotlin, see here.
Null Safety (or void safety) is the guarantee that no object reference will have a null
value.
In object-oriented languages, access to objects is achieved through references. A typical function call is of the form object.func()
; object
denotes a reference to a certain object, and func
denotes a function call. At execution time, the reference to object
can be void
, leading to run-time exceptions (In the case of Java, a NullPointerException) and often abnormal termination of the program.
When developing Android applications in Java, NullPointerException (NPE) was a big problem. In fact, About one third of app crashes can be attributed to NPE. To see how it happens, let's take a look at the piece of Java code below:
String a = null;
if(a.length > 5) {
//do something
}
When the above code is run, an NPE will be thrown on line 2 because a null
object has no methods. To prevent an object from taking on a null
value, programmers typically resort to doing additional checks like this:
String a = null;
if(a != null && a.length > 5) {
//do something
}
And of course, that's all fine, until we want to do something more complex. Say Bob belongs to a department, and we want to get the name of the department manager. That will look like this:
String managerName = bob.department.manager.name;
Because each variable can be null
, to prevent the NPE we put the code in the following code block:
if(bob != null) {
Department department = bob.department;
if(department != null) {
Employee manager = department.manager;
if(manager != null) {
String name = manager.name;
if(name != null) {
//do something
}
}
}
}
The deep-nested if
statement reduces readability of our code.
The other way is to use Java Optionals. For the first example above, we can do
a.ifPresent(this::doSomething);
And for the deeply-nested if
statements, the verbosity can be reduced with
bob.map(Person::getDepartment)
.map(Person::getManager)
.map(Person::getName)
.ifPresent(Person::doSomething);
map()
is a method in Java Optionals class that applies the function inside the parentheses to the object that is calling it. If the object is not present, it will return an empty Optional.
Let's see how Kotlin deals with this issue while maintaining a simple and readable syntax.
In Kotlin, a type can be nullable or non-nullable, determined by the presence of a ?
. For example, an object of type String
is non-nullable, while an object of type String?
is nullable.
As the compiler catches null
assignments to non-nullable objects, the following would result in compilation error.
var firstString: String = "hello world"
firstString = null //compilation error
In comparison, the following assignment to a nullable type is allowed
var secondString: String? = "hello world"
secondString = null //okay
In the first case, we can safely call firstString.length
without having to worry about a NPE because firstString
can never be null
.
In the second case, secondString
can potentially be null
, so secondString.length
will result in a compilation error as the compiler see the danger of such statement and blocks it early.
Although non-nullable type is a strong feature in Kotlin, the interoperability with Java means that we have to use variables as nullable type sometimes. In the previous section, we seem to have hit an obstacle as the compiler blocks the call to secondString.length
. In this section we look at some ways of overcoming this problem.
Represented by ?.
, the safe call operator is used in this way
secondString?.length
This returns the length of secondString
if secondString
is not null
, and null
otherwise.
Now we can chain like this
bob?.department?.manager?.name
This chain will return null
if any of the variables inside the chain is null
.
Represented by ?:
, the Elvis operator is used in this way
val length = secondString?.length ?: -1
If the expression to the left of ?:
is not null, the Elvis operator (?.
) will return it as it is; else it will return a default value supplied (-1 in this case).
We also notice the use of safe call operator together with Elvis operator in the same statement.
But the Elvis operator is more powerful than this. return
and throw
statements are legitimate default values on the right side of the Elvis operator. So you can define your own error message to aid debugging. For example:
fun myFunc(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("Name expected")
// ...
}
Doing so like this can help programmers to check for function arguments before carrying on with the required computation.
At this point you may ask, "What if I still want my NPE?"
Represented by !!
, the not-null assertion operator is used in this way
val stringLength = secondString!!.length
The operator converts any value to a non-nullable type and throws an exception if the value is null. In the above example, stringLength
will be assigned the length of secondString
if secondString
is not null
; if secondString is null
, a NPE is thrown. Kotlin tries to reduce the number of NPEs thrown as it is a run-time exception that is difficult to debug, in addition to creating so many app crashes. Hence NPEs in Kotlin are explicitly asked for.
null
value, make it a non-nullable type!