Null safety or how to avoid the “Billion-Dollar Mistake”
Back in 1965 Tony Hoare, the famous mind behind the Hoare Logic was working on a new programming language (ALGOL W) which has a concept that we still use today: the null
reference.
The problem with that null
reference is, that if developers are not careful enough to always check for a null
reference, we end up with the infamous NullPointerException
(at least in Java).
In his 2009 presentation Tony Hoare put it like this:
I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). 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. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
Kotlin (like some other more modern languages) was designed to be null-safe by allowing the compiler to check if a type is supposed to be nullable
or not. If a type is not nullable
then it must never be null
. Instead of a runtime exception that we only encounter when the software is already running, in Kotlin we cannot even compile the code in case we made a mistake.
Consider the following example:
1
2
3
public static int sum(List<Integer> numbers) {
return numbers.stream().mapToInt(Integer::intValue).sum();
}
When we call this the usual way, this works like a charm:
1
2
3
4
5
6
7
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
System.out.println(sum(list));
}
It returns
1
6
as expected. But what happens, if we try to execute our test code with one number being null (which is syntactically and semantically correct, mind you!):
1
2
3
4
5
6
7
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(null); //<- !
list.add(3);
System.out.println(sum(list));
}
We of course get
1
2
3
4
5
6
Exception in thread "main" java.lang.NullPointerException
[...]
at java.base/java.util.stream.IntPipeline.reduce(IntPipeline.java:491)
at java.base/java.util.stream.IntPipeline.sum(IntPipeline.java:449)
at com.example.NullPointerDemo.sum(NullPointerDemo.java:19)
at com.example.NullPointerDemo.main(NullPointerDemo.java:15)
Same for this code:
1
2
3
public static void main(String[] args) {
System.out.println(sum(null));
}
So now we have to adjust our code to counter this:
1
2
3
4
5
6
7
8
public static int sum(List<Integer> numbers) {
if (numbers == null) throw new IllegalArgumentException("list of numbers must not be null");
return numbers.stream().peek(i -> {
if (i == null) {
throw new IllegalArgumentException("can't sum up numbers if one is null");
}
}).mapToInt(Integer::intValue).sum();
}
Ugly? Isn’t it? And even with these additional checks in place we don’t get any advantage during compile time. Only the error messages get a little better during runtime:
1
2
3
4
5
6
7
Exception in thread "main" java.lang.IllegalArgumentException: can't sum up numbers if one is null
at com.example.NullPointerDemo.lambda$sum$0(NullPointerDemo.java:25)
[...]
at java.base/java.util.stream.IntPipeline.reduce(IntPipeline.java:491)
at java.base/java.util.stream.IntPipeline.sum(IntPipeline.java:449)
at com.example.NullPointerDemo.sum(NullPointerDemo.java:27)
at com.example.NullPointerDemo.main(NullPointerDemo.java:15)
Introducing Kotlin’s null-safe type system: here all types implicitly don’t allow null
values unless explicitly specified using the ?
type modifier. So our example becomes:
1
2
3
fun sum(numbers: List<Integer>): Integer {
return numbers.sum()
}
So a call to sum like this will yield a compile-time error and not compile:
1
2
3
fun main() {
sum(null)
}
or even when just one of the numbers is null it will not compile:
1
2
3
fun main() {
sum(listOf(1, null, 3))
}
Handy, isn’t it? But wait there is more!
Extension functions
Did you ever have a thought like this:
Why did the developer of this library class not add the method xyz() that would be so handy!
I guess every developer has had this thoughts from time to time. But with Java there is nothing that can be done about it short of writing some utility functions. Worse even if these utility functions live in different classes and are hard to find for developers that want to use them. Sometimes you even see the same utility functions implemented multiple times because it was just not visible enough.
In Kotlin however this becomes a very easy task: you can actually write new methods for classes, even for classes that are not in your source but reside in some library! How? By using extension functions:
Suppose you need a method in the built-in String
class that adds a certain string in front and after. Then you would simply define it somewhere in your code like this:
1
2
3
fun String.pad(str: String): String {
return str + this + str
}
now you can simply use it like so:
1
2
3
fun main() {
println(" after ".pad("time"))
}
and get the result
1
time after time
Smart Casts
In Kotlin, smart casts are a powerful feature that allows the compiler to automatically cast a variable to a more specific type when it’s guaranteed to be of that type. This eliminates the need for developers to perform explicit casting and makes the code cleaner and more concise.
The Kotlin compiler can smart cast a variable after performing type checks such as is
or !is
or when it determines that the variable cannot be modified between the check and the usage.
Consider the following example:
1
2
3
4
5
6
7
8
9
10
11
12
fun main() {
val obj: Any = "This is a string"
if (obj is String) {
// obj is smart cast to String within the if block
// and therefore we can access its length() method
// without an explicit cast
val len = obj.length
println("The length of the string is $len")
}
}
In Java an explicit downcast to String
would have been required, otherwise you wouldn’t be able to access the length()
method as Object doesn’t have it.
Kotlin is More Concise and Expressive
Kotlin was designed with the goal of being a more modern, expressive, and concise alternative to Java. It achieves this by introducing various language features and syntax improvements that reduce boilerplate code and make the code easier to read and write. Some examples we already saw by using null
-safe types or by introducing our own extension methods. Another example is how data containing POJO classes can be simplified in Kotlin by using the data class
type:
Consider a simple data class representing a person in both Java and Kotlin:
In java we would write something like this to describe a person with a name and an age:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
While in Kotlin this all boils down to this one line:
1
data class Person(val name: String, var age: Int)
The data class
declaration in Kotlin automatically generates all the necessary getters, setters, equals()
, hashCode()
, and toString()
methods, resulting in significantly less boilerplate code compared to the Java version. This makes the Kotlin code easier to read, write, and maintain.
Summary
In conclusion, at least for us, Kotlin has proven with quite a few new projects now, to be a more concise, expressive, and powerful language compared to Java. Its null-safety features help prevent the billion-dollar mistake of null
references by providing compile-time checks and eliminating the need for cumbersome null
checks in the code. The introduction of extension functions allows developers to easily add new methods to existing classes, even those from libraries, which promotes cleaner and more organized code. And smart casts simplify type casting by automatically casting variables to a more specific type when it’s safe to do so, again making the code more concise and less error-prone.
Moreover, Kotlin’s syntax improvements and language features like data classes significantly reduce boilerplate code and make the code easier to read, write, and maintain. These advantages make Kotlin a better choice for us to develop new software with. It addresses many of the pain points found in Java but also provides a modern and expressive programming experience. By adopting Kotlin, developers can enjoy increased productivity, more robust code, and ultimately, create better software. And isn’t that what we all strive for?