Expressions with Nullable Types
Of course, there will be times when we have to cope with null
. We want to do certain things if the value is not null
, and do something else if the value is null
. There are a variety of things in Kotlin that can help you work with null
values.
Intrinsically Safe Stuff
There are many things in Kotlin, supplied by the language or its standard library of classes and functions, that support null
. This includes things that you might not expect.
For example, CharSequence?
(from which String?
inherits) has an isNullOrEmpty()
function. As one might expect, it returns true
if the value is null
or has no characters (e.g., it is the empty string, ""
). So, this works:
fun foo(message: String?) {
println(message.isNullOrEmpty())
}
fun main() {
foo("Hello, world!")
foo("")
foo(null)
}
We get:
false
true
true
Not everything is allowed — many functions are defined on the core non-nullable type, rather than on its nullable counterpart.
So, for example, Int
has a dec()
function that returns the value decremented by one. That is defined on Int
, not Int?
, and so this code does not work:
val one = 1
println(one.dec())
val maybeZero : Int? = null
println(maybeZero.dec())
But, since this is a compile-time error, there is no harm in trying! And the compile error hints at a couple of solutions to the problem:
error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Int?
println(maybeZero.dec())
^
Safe Calls
Normally, to call a function on an object, you use a dot notation (.
).
One option for calling a function on a variable, parameter, or property that is of a nullable type is to use the safe-call operator (?.
). Then, one of two things will happen:
- If the value is
null
, your function call is ignored, andnull
is the result - If the value is not
null
, your function call is made as normal
So, this works:
val one : Int? = 1
println(one?.dec())
val maybeZero : Int? = null
println(maybeZero?.dec())
This prints:
0
null
So, the first dec()
call happens as normal, because one
is not null
, and the second dec()
call is replaced by null
, as maybeZero
is null
.
The type of the result of a ?.
function call is a nullable edition of whatever type the function call normally returns. Calling dec()
on an Int
returns an Int
, but calling dec()
on an Int
via ?.
returns an Int?
.
This means that you can chain ?.
calls:
val three : Int? = 3
println(three?.dec()?.dec()?.dec())
val maybeZero : Int? = null
println(maybeZero?.dec()?.dec()?.dec())
This prints the same result:
0
null
The Elvis Operator
?.
function calls behave differently depending on whether the “receiver” (the object on which you are calling the function) is null
or not.
Similarly, the “Elvis operator” — ?:
— returns different values depending on whether the left-hand side of the operation is null
or not:
- If the left-hand side is not
null
, the Elvis operator returns the left-hand value - If the left-hand side is
null
, the Elvis operator returns the right-hand value
val one : Int? = 1
println(one ?: "um, this should not be printed")
val maybeZero : Int? = null
println(maybeZero ?: "Elvis has not left the building")
This prints:
1
Elvis has not left the building
In the first println()
call, one
is not null
, so we print the value of one
. In the second println()
call, maybeZero
is null
, so we print the string that appears to the right of the operator.
Note that return
and throw
are valid things to have on the right-hand side of an Elvis operator:
fun printOrNot(value: Int?) {
val nonNullable = value ?: return
println(nonNullable)
}
fun main() {
val one : Int? = 1
printOrNot(one)
val maybeZero : Int? = null
printOrNot(maybeZero)
}
Here, we only get one line of output:
1
In the case where we call printOrNot()
with a null
value, the first line of the function will return
, bypassing the println()
call.
But now, a quick FAQ:
Why Is This Called the “Elvis Operator”?
If you turn your head to the side and look at the ?:
operator, it looks a bit like a pair of eyes, above which is a pompadour hairstyle. Elvis Presley famously wore his hair in a pompadour in his early career.
Who is Elvis Presley?
Ask your parents.
My Parents Are Asking: Who is Elvis Presley?
Elvis Presley was an American rock-and-roll icon of the 1950’s through 1970’s.
Isn’t This FAQ a Bit of a Sidetrack for a Programming Book?
Null Checks and Smart Contracts
Kotlin’s compiler can help avoid some of the pain of dealing with nullable types. If the compiler knows that something cannot be null
, due to some prior check, it relaxes the rules requiring safe calls (e.g., ?.
).
For example, let’s see if our Int?
is even, odd, or null
:
fun evenOrOddOrNull(value: Int?) {
if (value != null) {
if (value.rem(2) == 0) {
println("Even!")
}
else {
println("Odd!")
}
}
else {
println("Null!")
}
}
fun main() {
val one : Int? = 1
evenOrOddOrNull(one)
val maybeZero : Int? = null
evenOrOddOrNull(maybeZero)
}
rem()
is a function on Int
that takes another Int
, divides the two, and returns the remainder. So, rem(2)
will return 0
for even numbers and 1
for odd numbers.
This prints what you might expect:
Odd!
Null!
However, it may not be obvious why this even compiles. value
is an Int?
. It would seem that value.rem(2)
should fail with the same sort of compiler error that we saw earlier in the chapter (“only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Int?”).
However, we only try calling rem()
on value
inside of a null
check (if (value != null)
). Kotlin’s compiler knows that inside of that block, value
cannot be null
, because we just checked to see if it was null
or not. Kotlin’s compiler therefore relaxes the safe-call requirement, and we can just use .
for our rem()
call, rather than ?.
(and have to deal with a potentially null
result).
This only works when the compiler is sure that the value cannot be null
, though. That happens quite a bit, but there will be cases when the compiler cannot be certain and defaults to the safe-call requirement:
fun numberizer(): Int? = 1
fun evenOrOddOrNull() {
if (numberizer() != null) {
if (numberizer().rem(2) == 0) {
println("Even!")
}
else {
println("Odd!")
}
}
else {
println("Null!")
}
}
evenOrOddOrNull()
Here, we get our number from a numberizer()
function. That function is declared to return an Int?
, even though its implementation happens to always return 1
. Kotlin’s compiler does not attempt to examine the implementation of numberizer()
. It looks at the return type, sees that it is Int?
, and assumes that it could be null
. More importantly, just because if (numberizer() != null)
succeeded and we went into the if
block, the compiler has no guarantee that some future numberizer()
call will return a non-null
value, so it gives us a compile error for numberizer().rem(2)
, demanding a safe call there.
Dammit, It’s Not Null
Sometimes, though, you know better than the compiler. You are sure that a certain value is not null
, even though the compiler thinks otherwise.
For that, there is !!
. You can use this operator at the end of a value or expression, and it asserts to the compiler that the value or expression will not be null
, even if from a type standpoint it could be.
The previous example is a case where you know that numberizer()
could never return null
, but the compiler does not. The right solution in this case would be to have numberizer()
return Int
instead of Int?
. However, there will be cases where you do not have control over the return type of the function, so you cannot change it.
We can “fix” the previous example another way, via !!
:
fun numberizer(): Int? = 1
fun evenOrOddOrNull() {
if (numberizer() != null) {
if (numberizer()!!.rem(2) == 0) {
println("Even!")
}
else {
println("Odd!")
}
}
else {
println("Null!")
}
}
evenOrOddOrNull()
Here, we append !!
to the second numberizer()
call, to force the compiler to treat it as returning an Int
instead of as returning an Int?
. Hence, this snippet compiles and runs.
We also saw this in the preceding chapter, where we were forcing a NullPointerException
:
var thisIsReallyNull: String? = null
println(thisIsReallyNull)
println(thisIsReallyNull!!.length)
Here, we are asserting that thisIsReallyNull
is not null
via !!
. That results in a NullPointerException
, since thisIsReallyNull
is really null
.
!!
is a bit of a “code smell”. Use it sparingly, as if you are ever wrong in your assertion, you will crash with a NullPointerException
or the equivalent, as we did in the above snippet.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.