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:
- the compiler forces us to deal with potential
null
values and prevent NPE's - 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 variantT?
, e.g.Int
andInt?
- Expressions of type
T
can never containnull
- Expressions of type
T?
can containnull
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
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
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