Kotlin enforces null safety during compilation, statically verifying that you'll never see a NullPointerException
originating from Kotlin code[1].
To achieve achieve this, Kotlin needs to know whether a reference can be nullable. By default, references to types are considered to be non-null and attempting to assign null
to them will result in a compilation error. Nullable references are declared using a ?
annotation following the type name. e.g.
var a : String = "hello"
a = null // Error: Null can not be a value of a non-null type String
var b : String? = "hello"
b = null // Compiles
Furthermore, Kotlin protects you from calling a function on a nullable reference that could result in a NullPointerException
.
var a : String = "hello"
a.size // Compiles
var b : String? = "hello"
b.size // Error: Only safe (?.) or non-null asserted (!!.) calls are allowed...
As hinted at by the error message, Kotlin has dedicated operators for handling nulls. The following sections will explore these operators along with other approaches to null handling.
[1] Kotlin avoids NullPointExceptions
assuming you don't throw NullPointerExceptions
manually, and don't override the type system with the !!.
operator
The Safe call operator: ?.
The safe call operator ?.
will return null
if the object is null
, or otherwise the result of the expression. e.g.
var a : String? = ...
val size = a?.size // Resulting type will be Int?, not Int
Safe call operators can be chained together. If any of the expressions evaulates to null
then the result will be null
. e.g.
val number = person?.address?.street?.number // Will be null if any property is null
The Sure operator: !!.
If you're sure that an object can't be null
at a particular statement, you can override the type system with the !!.
operator. e.g.
var a : String? = ...
val size = a!!.size // Resulting type will be Int
Note: This operation is unsafe and will immediately throw a NullPointerException
if the object is null
.
The Elvis operator: ?:
The Elvis operator ?:
allows you to supply a default value in the case of null. e.g.
var a : String? = ...
val size = a?.size ?: -1 // Resulting type will be Int, -1 when a is null
The Elvis operator can also be combined with a throw
clause. e.g.
var a : String? = ...
val size = a?.size ?: throw IllegalArgumentException() // Type will be Int
Data flow analysis
The Kotlin compiler performs limited data flow analysis enabling it to automatically determine if a variable can't be null at a particular statement. e.g.
var a : String? = ...
if (a != null) {
val size = a.size // Type will be Int, no need for ?. or !!. operators
}
Presently, the data flow analysis is pretty limited. e.g. common Java idoms of using checkNotNull
methods will not be detected as non-null.
var a : String? = ...
checkNotNull(a, "a should never be null here")
val size = a.size // Won't compile as analysis won't extend into checkNotNull
However, Kotlin extension functions do provide an elegant way of implementing such checks in a fashion that the type system can understand.
Extension functions
Extension functions in Kotlin allow you to add methods to existing types. This includes Any
, the root of all types in Kotlin. By applying a non-null upper bound (<T: Any>
) and defining the method on nullable T?
, you can effectively add methods to any nullable type.
Below I've defined two methods for nullable types that I find useful:
orThrow
: throws an exception if the reference is null, otherwise returns the non-null resultorError
: throws an IllegalArgumentException if the reference is null, otherwise returns the non-null result
public inline fun <T : Any> T?.orThrow(throwable: Throwable): T {
return if (this == null) throw throwable else this
}
public inline fun <T : Any> T?.orError(message: String): T {
return if (this == null) throw IllegalArgumentException(message) else this
}
And here's some examples of usage:
val a : String? = ...
val b = a.orThrow(MyException()) // b & c will be of type String, not String?
val c = a.orError("You must supply a value for 'a'")
Extensions functions also provide a mechanism to satisfy those who complain that Kotlin's null handling isn't correct as it's not an Option
class or a monad, etc. If you really want an Option
class solution, you can use an extension function to convert any nullable type to an option.
public inline fun <T : Any> T?.toOption(): Option<T> {
return if (this == null) None() else Some(this)
}
Delegated Properties
Finally, there are situations where your intention is that an object will not be nullable but you can't supply the value at the point of declaration — the object is initialised after construction.
class Foo {
var bar : Bar? = null // Works, but now type is Bar?
fun initialise() {
bar = Bar()
}
}
This works, but now you have to handle the null
case every time you use the bar
variable.
Kotlin has a feature called Delegated Properties that allows you to customise the behaviour of properties, by providing your own Delegate
implementation that handles get
and set
operations.
One built-in delegate is notNull()
. This effectively provides a non-null placeholder that alleviates the need to deal with nulls — as long as you set it before you use it!
class Foo {
var bar : Bar by kotlin.properties.Delegates.notNull() // type is now Bar
fun initialise() {
val s = bar // Using before setting throws an IllegalStateException!
bar = Bar()
}
}
Conclusion
Kotlin can statically verify your program is null-safe during compilation — providing you stick to safe operations. It also provides a rich set of operators, extensions and delegates to make the reality of handling nulls as pleasant as it can be. Finally, Kotlin is a pragmatic language, allowing you override the type system as required.