Kotlin Primer
Nullability
Java doesn't have a good way to tell if a variable or parameter can be null or not. There are a couple of annotations you can add, but unless you specify them explicitly everywhere, any variable or parameter could receive a null (and the annotation processing to check those annotations isn't exhaustive).
Kotlin fixes this. Every type you specify is either explicitly nullable or non-nullable.
When you specify a type normally, it's explicitly non-nullable.
fun doSomething(text: String) { // passed text must not be null!!!
println(text)
println(text.length) // following the text pointer is guaranteed ok
}
fun main() {
doSomething("Hello")
doSomething(null) // compiler error here!!!
}
If you add ? after the type name, it's explicitly nullable.
fun doSomething(text: String?) { // can pass null!
println(text)
println(text.length)
// following the text pointer is NOT ok - could be null
// compiler error here!!!
}
fun main() {
doSomething("Hello")
doSomething(null) // compiles ok
}
So how do you handle nullable data? There are a few options
- Assert the value is non-null
- Use the safe-access operator
- Use the Elvis operator to supply an alternative value
Let's start with a very shady approach, one that you should almost never use...
Asserting non-null
Danger
This is shady. That deserves being said even again. Shady. Very shady. Don't do this!!!
There may be times in your program where a property or parameter is nullable, but you know
for certain at some points in your logic that it cannot be null. You can tell the compiler
"trust me, it's not null" by using the not-null assertion operator !! (pronounced "bang bang")
fun doSomething(text: String?) { // text is nullable
println(text)
println(text!!.length)
// tells compiler "trust me, it's really not null!"
// compiler allows access
// will double-check at runtime
}
fun main() {
var value: String?
value = "Hello"
doSomething(value)
value = null
doSomething(value)
}
We're saying "I know text reaaaaaaallly isn't null, even though the type of the thing being passed
in could be nullable"
The compiler will allow this, and the runtime will double-check. If it ends up null, a
NullPointerException will be thrown at runtime. You really don't want that.
This is generally a horrible idea, and should be avoided at all costs. There are plenty of better ways to handle null values, which give you direct control.
Checking with if
Suppose we just check using an if expression (yes - I said "expression" - more on this later...).
fun doSomething(text: String?) { // text is nullable
println(text)
if (text != null) {
println(text.length)
}
}
fun main() {
var value: String?
value = "Hello"
doSomething(value)
value = null
doSomething(value)
}
This works! But if we do this in a class on a var property:
class Person(
var name: String?,
) {
fun doSomething() {
println(name)
if (name != null) {
println(name.length)
}
}
}
fun main() {
Person("Scott").doSomething()
Person(null).doSomething()
}
The compiler will give us a very interesting message:
Smart cast to 'String' is impossible, because 'name' is a mutable property that could be mutated concurrently.
What's going on here???
Kotlin has some pretty great data flow analysis. It watches for how data is defined and how it's
used, and considers potential concurrent processing. A Person instance could be shared across two
threads (or better, coroutines) that run at the same time. There's an insidious gap between the
if (name != null) check and the name.length de-reference. In that gap, a different thread or
coroutine could, in fact, set the value to null, and we'd blow up. Kotlin has saved us from a
nasty concurrency bug!
But why does the first if example work? And what's this "smart cast" business?
When we write
fun doSomething(text: String?) { // text is nullable
println(text)
if (text != null) {
println(text.length)
}
}
there's no way for text to change inside the function. Parameters are immutable inside functions;
text (a pointer to a nullable String) cannot change. The compiler knows and enforces this,
and can then make some very smart decisions about its use.
In this function, we call if (text != null) {...}. Because text cannot change, the compiler
knows that inside that block, text cannot be null. Because of this, it automatically casts it
from String? to String, and we can use it as a non-nullable pointer.
This is known as a "smart cast". In
class Person(
var name: String?,
) {
fun doSomething() {
println(name)
if (name != null) {
println(name.length)
}
}
}
the compiler knows that name can change, and cannot assure that it will be non-null inside the
if block. It cannot perform the smart cast, and we're stuck with the type of name being
String?.
One way around this is the "capture, then check" approach:
class Person(
var name: String?,
) {
fun doSomething() {
println(name)
val capturedName = name
if (capturedName != null) {
println(capturedName.length)
}
}
}
fun main() {
Person("Scott").doSomething()
Person(null).doSomething()
}
Here we grab a point-in-time snapshot of the value of name as a val. When checking and using
that val, Kotlin can perform its smart cast, and we see its type as String inside the if
block.
Because this is such a common pattern, it has been captured as a "scope function" called let
that we'll see later on.
Use the safe-access operator
The safe-access operator ?. allows us to write a de-reference expression on a nullable value.
fun main() {
var value: String?
value = "Hello"
println(value?.length)
value = null
println(value?.length)
}
If the operand to the left of ?. is non-null, the expression will continue evaluating. If the
operand is null, evaluation will stop and return null.
In the above, when value is non-null, we proceed to process value.length. If value is null,
we immediately stop and return null.
This can chain further. For example
class Person(
var name: String?,
)
fun main() {
var person: Person?
person = Person("Scott")
println(person?.name?.length)
person = Person(null)
println(person?.name?.length)
person = null
println(person?.name?.length)
}
As soon as null is seen, the entire expression is halted and null is its result.
Use the Elvis operator to supply an alternative value
But what if we want to provide an alternative? The Elvis operator (named because if you turn your head 90 degrees to the left and look at it, you'll see Elvis Presley's hair and eyes (don't blame me; I didn't name it...)), provides an alternative.
When you write value ?: alternativeValue, if value is non-null, value is the result of
the Elvis operator. If value is null, alternativeValue is the result.
class Person(
var name: String?,
)
fun nameLength(person: Person?): Int {
return person?.name?.length ?: 0
}
fun main() {
var person: Person?
person = Person("Scott")
println(nameLength(person))
person = Person(null)
println(nameLength(person))
person = null
println(nameLength(person))
}
We'll be talking more about functions in a bit, but here I want you to take a look at the return
type of nameLength. It's an Int (primitive integer that acts like an integer Class). If we
hadn't used Elvis
class Person(
var name: String?,
)
fun nameLength(person: Person?): Int? {
return person?.name?.length
}
fun main() {
var person: Person?
person = Person("Scott")
println(nameLength(person))
person = Person(null)
println(nameLength(person))
person = null
println(nameLength(person))
}
the return type would have to be Int?, which could have some serious consequences for its caller.
Note
"Consequences" here isn't necessarily a bad thing; returning Int? would be useful in
some circumstances, as it allows you to know that the length is not able to be computed,
vs treating it as though we had an empty String.
Speaking of empty String, we could use Elvis earlier...
class Person(
var name: String?,
)
fun nameLength(person: Person?): Int {
return (person?.name ?: "").length
}
fun main() {
var person: Person?
person = Person("Scott")
println(nameLength(person))
person = Person(null)
println(nameLength(person))
person = null
println(nameLength(person))
}
Here we get the same results, and to be honest, I'd let the nulls flow through to the end as we
had been doing. I wanted to show this example as sometimes it's useful to de-nullify earlier,
as there might be a good default value you can inject midway through a de-reference chain.