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. (Excerpt from Wikipedia)
-Sir Tony Hoare, The inventor of null reference
If you have been programming in Java or any other language that has the concept of null reference then you must have heard about or experienced NullPointerException
in your programs.
NullPointerExceptions are Runtime Exceptions which are thrown by the program at runtime causing application failure and system crashes.
Wouldn’t it be nice if we could detect possible NullPointerException
exception errors at compile time itself and guard against them?
Well, Enter Kotlin!
Nullability and Nullable Types in Kotlin
Kotlin supports nullability as part of its type System. That means You have the ability to declare whether a variable can hold a null value or not.
By supporting nullability in the type system, the compiler can detect possible NullPointerException errors at compile time and reduce the possibility of having them thrown at runtime.
Let’s understand how it works!
All variables in Kotlin are non-nullable by default. So If you try to assign a null value to a regular variable, the compiler will throw an error -
var greeting: String = "Hello, World"
greeting = null // Compilation Error
To allow null values, you have to declare a variable as nullable by appending a question mark in its type declaration -
var nullableGreeting: String? = "Hello, World"
nullableGreeting = null // Works
We know that NullPointerException
occurs when we try to call a method or access a property on a variable which is null
. Kotlin disallows method calls and property access on nullable variables and thereby prevents many possible NullPointerExceptions.
For example, The following method access works because Kotlin knows that the variable greeting
can never be null -
val len = greeting.length
val upper = greeting.toUpperCase()
But the same method call won’t work with nullableGreeting
variable -
val len = nullableGreeting.length // Compilation Error
val upper = nullableGreeting.toUpperCase() // Compilation Error
Since Kotlin knows beforehand which variable can be null and which cannot, It can detect and disallow calls which could result in NullPointerException
at compile-time itself.
Working with Nullable Types
All right, It’s nice that Kotlin disallows method calls and property access on nullable variables to guard against NullPointerException errors. But we still need to do that right?
Well, There are several ways of safely doing that in Kotlin.
1. Adding a null Check
The most trivial way to work with nullable variables is to perform a null check before accessing a property or calling a method on them -
val nullableName: String? = "John"
if(nullableName != null) {
println("Hello, ${nullableName.toUpperCase()}.")
println("Your name is ${nullableName.length} characters long.")
} else {
println("Hello, Guest")
}
Once you perform a null comparison, the compiler remembers that and allows calls to toUpperCase()
and length
inside the if
branch.
2. Safe call operator: ?.
Null Comparisons are simple but too verbose. Kotlin provides a Safe call operator, ?.
that reduces this verbosity. It allows you to combine a null-check and a method call in a single expression.
For example, The following expression -
nullableName?.toUpperCase()
is same as -
if(nullableName != null)
nullableName.toUpperCase()
else
null
Wow! That saves a lot of keystrokes, right? :-)
So if you were to print the name in uppercase and its length safely, you could do the following -
val nullableName: String? = null
println(nullableName?.toUpperCase())
println(nullableName?.length)
// Prints
null
null
That printed null
since the variable nullableName
is null, otherwise, it would have printed the name in uppercase and its length.
But what if you don’t want to print anything if the variable is null
?
Well, To perform an operation only if the variable is not null, you can use the safe call operator with let
-
val nullableName: String? = null
nullableName?.let { println(it.toUpperCase()) }
nullableName?.let { println(it.length) }
// Prints nothing
The lambda expression inside let
is executed only if the variable nullableName
is not null.
That’s great but that’s not all. Safe call operator is even more powerful than you think. For example, You can chain multiple safe calls like this -
val currentCity: String? = user?.address?.city
The variable currentCity
will be null if any of user
, address
or city
is null. (Imagine doing that using null-checks.)
3. Elvis operator: ?:
The Elvis operator is used to provide a default value when the original variable is null
-
val name = nullableName ?: "Guest"
The above expression is same as -
val name = if(nullableName != null) nullableName else "Guest"
In other words, The Elvis operator takes two values and returns the first value if it is not null, otherwise, it returns the second value.
The Elvis operator is often used with Safe call operator to provide a default value other than null
when the variable on which a method or property is called is null
-
val len = nullableName?.length ?: -1
You can have more complex expressions on the left side of Elvis operator -
val currentCity = user?.address?.city ?: "Unknown"
Moreover, You can use throw
and return
expressions on the right side of Elvis operator. This is very useful while checking preconditions in a function. So instead of providing a default value in the right side of Elvis operator, you can throw an exception like this -
val name = nullableName ?: throw IllegalArgumentException("Name can not be null")
4. Not null assertion : !! Operator
The !! operator converts a nullable type to a non-null type, and throws a NullPointerException
if the nullable type holds a null value
.
So It’s a way of asking for NullPointerException
explicitly. Please don’t use this operator.
val nullableName: String? = null
nullableName!!.toUpperCase() // Results in NullPointerException
Null Safety and Java Interoperability
Kotlin is fully interoperable with Java but Java doesn’t support nullability in its type system. So what happens when you call Java code from Kotlin?
Well, Java types are treated specially in Kotlin. They are called Platform types. Since Kotlin doesn’t have any information about the nullability of a type declared in Java, It relaxes compile-time null checks for these types.
So you don’t get any null safety guarantee for types declared in Java, and you have full responsibility for operations you perform on these types. The compiler will allow all operations. If you know that the Java variable can be null, you should compare it with null before use, otherwise, just like Java, you’ll get a NullPointerException at runtime if the value is null.
Consider the following User class declared in Java -
public class User {
private final String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Since Kotlin doesn’t know about the nullability of the member variable name
, It allows all operations on this variable. You can treat it as nullable or non-nullable, but the compiler won’t enforce anything.
In the following example, We simply treat the variable name
as non-nullable and call methods and properties on it -
val javaUser = User(null)
println(javaUser.name.toUpperCase()) // Allowed (Throws NullPointerException)
println(javaUser.name.length) // Allowed (Throws NullPointerException)
The other option is to treat the member variable name
as nullable and use the safe operator for calling methods or accessing properties -
val javaUser = User(null)
println(javaUser.name?.toUpperCase()) // Allowed (Prints null)
println(javaUser.name?.length) // Allowed (Prints null)
Nullability Annotations
Although Java doesn’t support nullability in its type system, You can use annotations like @Nullable
and @NotNull
provided by external packages like javax.validation.constraints
, org.jetbrains.annotations
etc to mark a variable as Nullable or Not-null.
Java compiler doesn’t use these annotations, but these annotations are used by IDEs, ORM libraries and other external tools to provide assistance while working with null values.
Kotlin also respects these annotations when they are present in Java code. Java types which have these nullability annotations are represented as actual nullable or non-null Kotlin types instead of platform types.
Nullability and Collections
Kotlin’s collection API is built on top of Java’s collection API but it fully supports nullability on Collections.
Just as regular variables are non-null by default, a normal collection also can’t hold null values -
val regularList: List<Int> = listOf(1, 2, null, 3) // Compiler Error
1. Collection of Nullable Types
Here is how you can declare a Collection of Nullable Types in Kotlin -
val listOfNullableTypes: List<Int?> = listOf(1, 2, null, 3) // Works
To filter non-null values from a list of nullable types, you can use the filterNotNull()
function -
val notNullList: List<Int> = listOfNullableTypes.filterNotNull()
2. Nullable Collection
Note that there is a difference between a collection of nullable types and a nullable collection.
A collection of nullable types can hold null values but the collection itself cannot be null -
var listOfNullableTypes: List<Int?> = listOf(1, 2, null, 3) // Works
listOfNullableTypes = null // Compilation Error
You can declare a nullable collection like this -
var nullableList: List<Int>? = listOf(1, 2, 3)
nullableList = null // Works
3. Nullable Collection of Nullable Types
Finally, you can declare a nullable collection of nullable types like this -
var nullableListOfNullableTypes: List<Int?>? = listOf(1, 2, null, 3) // Works
nullableListOfNullableTypes = null // Works
Conclusion
That’s all in this article folks. I hope you understood how kotlin helps you avoid NullPointerException errors with its nullable type concept.
Thanks for reading. See you in the next post.