🏎️ · Nullability βš™οΈ

6 min read Β· Updated on by

Read on to learn about the concept of nullability in Kotlin. Understand what nullable and non-nullable types are, along with the safe-call, elvis, and bang-bang operators.

The purpose of nullable types is to explicitly designate places where null values are allowed. This has two benefits:

  1. the compiler forces us to deal with potential null values and prevent NPE's
  2. the compiler guarantees that expressions of a non-nullable type cannot be null, saving us from writing inordinate amounts of defensive code everywhere
  • Every Kotlin type T has a nullable variant T?, e.g. Int and Int?
  • Expressions of type T can never contain null
  • Expressions of type T? can contain null

If you’re using IDEA, select any expression and use this shortcut to display its type. This is incredibly useful, and I encourage you to use it liberally.


//sampleStart
// This does not compile
val notNullableString: String = null
// This is allowed
val nullableString: String? = null

// This is fine, since notNullableString can never be null
println(notNullableString.length)
// This will not compile, since nullableString could be null, which would cause a NPE
println(nullableString.length)

// This works, since the compiler can prove that nullableString is not null when the length property is accessed
println(if(nullableString != null) nullableString.length else null)

// The variable 'list' is of type List<Int>, which means that a) it is never null, and b) never contains a null value.
// Think: what do List<Int?>, List<Int>? and List<Int?>? mean?
fun getFirstPositiveInt(list: List<Int>): Int? {
    for (elem in list) {
        if (elem > 0) return elem
    }
    return null
}
//sampleEnd

The safe call operator

Take a look at this again:


//sampleStart
println(if(nullableString != null) nullableString.length else null)
//sampleEnd

This is such a common pattern that Kotlin defines the safe call operator ?. which achieves the same thing:


//sampleStart
println(nullableString?.length)
//sampleEnd

The expression x?.y evaluates to x.y when x is not null, and null otherwise. It is equivalent to if(x != null) x.y else null

The elvis operator

Often, we would like to control the β€œfallback” value of the code we were just discussing, for instance:


//sampleStart
println(if(nullableString != null) nullableString.length else 0)
//sampleEnd

Kotlin has the elvis operator ?:, which when combined with ?. achieves exactly that:


//sampleStart
println(nullableString?.length ?: 0)
//sampleEnd

The expression x ?: y evaluates to x if x is not null, and y otherwise. It is equivalent to if(x != null) x else y.

The bang-bang operator

There are situations where we know an argument isn’t null, even though the compiler can’t prove it:


// You'll learn about stdlib functions such as 'first', and the 'it' parameter, in future articles. 
// Don't worry about them for now.
fun firstPositiveIntOrNull(list: List): Int? = list.first { it > 0 }
//sampleStart
fun main() {
    // For this particular argument, we know that the result is non-null, 
    // however the compiler can't infer this generally, so the type of 'result' is still Int?
	val result = firstPositiveIntOrNull(listOf(1, 2, 3))
	println(result)
}
//sampleEnd

To deal with these situations, Kotlin introduces the not-null-assertion operator !!, often called the bang-bang operator:


//sampleStart
// This works
val result2: Int = firstPositiveIntOrNull(listOf(1, 2, 3))!!
//sampleEnd

As in many religions, it is recommended to abstain from bang-banging, and only bang-bang when you really have to. Double-banging a null value throws a NPE.

I strongly believe that the use of this operator is almost always a shortcut that circumvents a deeper problem, and can be avoided if the deeper problem is fixed. If you feel like you've come across an instance where !! is unavoidable, please do share it in the comments!

Exercises

Implement canBangBangKotlin() in the same spirit as canBangBangJava(). Use the operators we just introduced.


import org.junit.Assert
import org.junit.Test

class TestBandBang() {
    
    val personWithAge = Person(
    	firstName = "firstName",
        lastName = "lastName",
        age = 32,
        phone = null
    )
    
    val personWithoutAge = Person(
    	firstName = "firstName",
        lastName = "lastName",
        age = null,
        phone = null
    )
    
    @Test(timeout = 1000)
    fun bangbang() {
        Assert.assertTrue("The function canBangBangKotlin is not implemented correctly", canBangBangKotlin(personWithAge, 32))
        Assert.assertFalse("The function canBangBangKotlin is not implemented correctly", canBangBangKotlin(personWithAge, 33))
        Assert.assertFalse("The function canBangBangKotlin is not implemented correctly", canBangBangKotlin(personWithoutAge, 33))
    }
}

class PhoneNumber(val prefix: String?, val number: String)

class Person(
    val firstName: String, val nickname: String? = null, val lastName: String,
    val age: Int?,
    val phone: PhoneNumber?
) {
    init {
        require(age ?: Int.MAX_VALUE > 0) { "Age cannot be negative" }
    }
}

//sampleStart
fun canBangBangJava(person: Person?, consentAge: Int): Boolean {
    if(person == null) {
        throw IllegalArgumentException("Person cannot be null");
    }

    if(person.age == null) {
        return false
    }

    return person.age >= consentAge
}

fun canBangBangKotlin(person: Person, consentAge: Int): Boolean = TODO("Implement canBangBangKotlin()!")
//sampleEnd

Solution

Implement formatPhoneKotlin() in the same spirit as formatPhoneJava().


import org.junit.Assert
import org.junit.Test

class TestBandBang() {
    
    val withPhonePrefix = PhoneNumber("+314", "123321")
    val withoutPhonePrefix = PhoneNumber(null, "123321")
    
    @Test(timeout = 1000)
    fun testFormatPhone() {
        Assert.assertEquals("The function formatPhoneKotlin is not implemented correctly", "+314 123321", formatPhoneKotlin(withPhonePrefix))
        Assert.assertEquals("The function formatPhoneKotlin is not implemented correctly", "+420 123321", formatPhoneKotlin(withoutPhonePrefix))
    }
}

class PhoneNumber(val prefix: String?, val number: String)

class Person(
    val firstName: String, val nickname: String? = null, val lastName: String,
    val age: Int?,
    val phone: PhoneNumber?
) {
    init {
        require(age ?: Int.MAX_VALUE > 0) { "Age cannot be negative" }
    }
}

//sampleStart
fun formatPhoneJava(phone: PhoneNumber?, defaultPrefix: String = "+420"): String {
    if(phone == null) {
        throw IllegalArgumentException("Phone cannot be null");
    }
    if(phone.number == null) {
        throw IllegalArgumentException("Phone.number cannot be null");
    }
    if(defaultPrefix == null) {
        throw IllegalArgumentException("defaultPrefix cannot be null");
    }

    if(phone.prefix != null) {
        return phone.prefix + " " + phone.number
    }

    return defaultPrefix + " " + phone.number
}

fun formatPhoneKotlin(phone: PhoneNumber, defaultPrefix: String = "+420") = TODO("Implement formatPhoneKotlin()")
//sampleEnd

Solution

Implement humanStrKotlin() in the same spirit as humanStrJava(). When you’re finished, take a look at the difference between the two and enjoy the feeling.


import java.lang.StringBuilder
import org.junit.Assert
import org.junit.Test

class TestBangBang() {
    
    val person1 = Person(
        firstName = "Honza", nickname = "PΓ‘rek", lastName = "PΓ‘rker",
        18,
        PhoneNumber(null, "123456789")
    )
    val person2 = Person(
        firstName = "Franta", lastName = "ZvadlΓ½",
        age = null,
        phone = null
    )
    
    @Test(timeout = 1000)
    fun testHumanStr() {
        Assert.assertEquals(
            "The function humanStrKotlin is not implemented correctly", 
            """
            |Name: Honza 'PΓ‘rek' PΓ‘rker
            |Age: 18
            |Phone: +420 123456789
            """.trimMargin(), 
            humanStrKotlin(person1)
        )
        
        Assert.assertEquals(
            "The function humanStrKotlin is not implemented correctly", 
            """
            |Name: Franta  ZvadlΓ½
			|Age: Unknown
			|Phone: Unknown
            """.trimMargin(), 
            humanStrKotlin(person2)
        )
    }
}

class PhoneNumber(val prefix: String?, val number: String)

class Person(
    val firstName: String, val nickname: String? = null, val lastName: String,
    val age: Int?,
    val phone: PhoneNumber?
) {
    init {
        require(age ?: Int.MAX_VALUE > 0) { "Age cannot be negative" }
    }
}

val Person.quotedNick get(): String? = nickname?.let {"'$it'" }
val Person.phoneString get(): String? = phone?.let(::formatPhone)
fun formatPhone(phone: PhoneNumber, defaultPrefix: String = "+420") = "${phone.prefix ?: defaultPrefix} ${phone.number}"

//sampleStart
fun humanStrJava(person: Person): String {
    if(person == null) {
        throw IllegalArgumentException("Person cannot be null");
    }

    val builder = StringBuilder()

    if(person.firstName == null) {
        throw IllegalArgumentException("Person.firstName cannot be null");
    }

    if(person.lastName == null) {
        throw IllegalArgumentException("Person.lastName cannot be null");
    }

    val quotedNick = if(person.quotedNick == null) "" else person.quotedNick!!
    builder.append("Name: ")
        .append(person.firstName).append(" ").append(quotedNick).append(" ").append(person.lastName)
        .append("\n")

    builder.append("Age: ")
        .append(if(person.age == null) "Unknown" else person.age.toString())
        .append("\n")

    builder.append("Phone: ")
        .append(if(person.phoneString == null) "Unknown" else person.phoneString)

    return builder.toString()
}

fun humanStrKotlin(person: Person) = """
    |Name: ${TODO("Implement correct name printing!")}
    |Age: ${TODO("Implement correct age printing!")}
    |Phone: ${TODO("Implement correct phone printing!")}
""".trimMargin()
//sampleEnd

Solution

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer