Null Safety Workbook

Ten short exercises on Kotlin null safety — safe calls, the Elvis operator, !!, safe casts, ?.let, lateinit, nullable collections, and platform types.

Practice problems for Kotlin’s Billion-Dollar Fix. Each takes a minute or two. Most exercises auto-check: implement the function in the editor and press Run — hidden tests go green when you’re right and red (with a hint) when you’re not, all on JetBrains’ Kotlin server. A few (the ones that declare a type or lean on Java) stay attempt-then-reveal: press Run to try them, then click Show answer. Nothing here is a trick question, just direct practice of the syntax from the lesson.

safe calls and Elvis

1. Safe chain

Implement cityOf so it returns the user’s city, or null if the user or their address is null.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Address(val city: String) class User(val address: Address?) class Test { @Test fun cityOf() { Assert.assertEquals("dig out the city through the safe chain", "Paris", cityOf(User(Address("Paris")))) Assert.assertEquals("null when the address is null", null, cityOf(User(null))) } } //sampleStart fun cityOf(user: User?): String? = null // TODO: get the city, or null if user or address is null (safe calls) //sampleEnd
Show answer Hide answer
fun cityOf(user: User?): String? = user?.address?.city

2. A fallback value

Implement nameOrDefault so it returns the name, or "unknown" when it’s null.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun nameOrDefault() { Assert.assertEquals("the name when present", "Ada", nameOrDefault("Ada")) Assert.assertEquals("the fallback when null", "unknown", nameOrDefault(null)) } } //sampleStart fun nameOrDefault(name: String?): String = "TODO" // TODO: produce name, or "unknown" when it is null (Elvis) //sampleEnd
Show answer Hide answer
fun nameOrDefault(name: String?): String = name ?: "unknown"

3. Elvis with an early return

Implement tokenOrNull so it returns request.token, or returns null early when the token is null.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Request(val token: String?) class Test { @Test fun tokenOrNull() { Assert.assertEquals("the token when present", "abc123", tokenOrNull(Request("abc123"))) Assert.assertEquals("null when the token is null", null, tokenOrNull(Request(null))) } } //sampleStart fun tokenOrNull(request: Request): String? { // TODO: bind token to request.token, or 'return null' when it is null (Elvis + return) return "TODO" } //sampleEnd
Show answer Hide answer
fun tokenOrNull(request: Request): String? {
    val token = request.token ?: return null
    return token
}

4. Length or zero

Implement lengthOrZero so it returns the string’s length, or 0 when it’s null.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun lengthOrZero() { Assert.assertEquals("the length when present", 5, lengthOrZero("hello")) Assert.assertEquals("zero when null", 0, lengthOrZero(null)) } } //sampleStart fun lengthOrZero(maybe: String?): Int = -1 // TODO: maybe's length, or 0 when null (safe call + Elvis) //sampleEnd
Show answer Hide answer
fun lengthOrZero(maybe: String?): Int = maybe?.length ?: 0

casts, let, and lateinit

5. Safe cast with a default

Implement asIntOrZero so it returns value as an Int, or 0 when it isn’t one.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun asIntOrZero() { Assert.assertEquals("the Int when it is one", 42, asIntOrZero(42)) Assert.assertEquals("zero when it isn't", 0, asIntOrZero("hello")) } } //sampleStart fun asIntOrZero(value: Any): Int = -1 // TODO: value as an Int, or 0 if it isn't one (safe cast + Elvis) //sampleEnd
Show answer Hide answer
fun asIntOrZero(value: Any): Int = value as? Int ?: 0

6. Run only when present

Given email: String?, call sendWelcome(email) only when it isn’t null.

Try it — edit, then press Run fun sendWelcome(email: String) { println("welcome " + email) } fun main() { val email: String? = "[email protected]" // TODO: call sendWelcome only when email is not null (use ?.let) if (email != null) sendWelcome(email) }
Show answer Hide answer
email?.let { sendWelcome(it) }

7. Assigned later

Declare a non-null repository: UserRepository you’ll assign in a separate setUp() method, without making it nullable.

Try it — edit, then press Run class UserRepository class Fixture { // TODO: declare a non-null repository assigned later, without making it nullable (lateinit) var repository: UserRepository? = null fun setUp() { repository = UserRepository() } } fun main() { val f = Fixture() f.setUp() println(f.repository) }
Show answer Hide answer
lateinit var repository: UserRepository

fun setUp() {
    repository = UserRepository()
}

collections and Java

8. Drop the nulls

Implement dropNulls so it returns a List<String> with the nulls removed.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun dropNulls() { Assert.assertEquals("keep only the non-null names", listOf("Ada", "Grace"), dropNulls(listOf("Ada", null, "Grace"))) } } //sampleStart fun dropNulls(names: List<String?>): List<String> = emptyList() // TODO: produce a List<String> with the nulls removed (filterNotNull) //sampleEnd
Show answer Hide answer
fun dropNulls(names: List<String?>): List<String> = names.filterNotNull()

9. Pin a Java result

A Java method javaApi.getName() returns a platform type. Assign it to a Kotlin val asserting it’s non-null (failing fast if it isn’t).

Try it — edit, then press Run fun main() { // System.getProperty returns a platform type (String!) // TODO: assign it to a val asserting non-null (fail fast if it is null) val vendor = System.getProperty("java.vendor") println(vendor) }
Show answer Hide answer
val name: String = javaApi.getName()

The explicit non-null type makes Kotlin check at this boundary instead of letting a null slip downstream.

10. The escape hatch

Given maybe: String? that you’re certain is set, get its length with a not-null assertion. What happens if you’re wrong?

Try it — edit, then press Run fun main() { val maybe: String? = "certain" // TODO: get maybe's length with a not-null assertion (!!) val len = maybe?.length ?: -1 println(len) }
Show answer Hide answer
maybe!!.length

If maybe is null, !! throws a NullPointerException on the spot. Reach for it rarely.


Going deeper: Elvis, collections, and nullable receivers

12. Elvis with an early return

?: can supply a return on its right side. Implement firstWord to return the trimmed first word of a nullable string, or "unknown" when it’s null — in one expression.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun firstWord() { Assert.assertEquals("Ada", firstWord(" Ada Lovelace ")) Assert.assertEquals("unknown", firstWord(null)) } } //sampleStart fun firstWord(full: String?): String = "" // TODO: trimmed first word, or "unknown" when null //sampleEnd
Show answer Hide answer
fun firstWord(full: String?): String =
    full?.trim()?.substringBefore(" ") ?: "unknown"

The safe calls short-circuit to null, and Elvis supplies the fallback.

13. Drop the nulls, change the type

Implement cleanLengths so it takes a List<String?>, drops the nulls, and returns the lengths of what’s left — and note the result type is List<Int>, not List<Int?>.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun clean() { Assert.assertEquals(listOf(1, 3), cleanLengths(listOf("a", null, "ccc"))) } } //sampleStart fun cleanLengths(xs: List<String?>): List<Int> = emptyList() // TODO: drop nulls, then map to lengths //sampleEnd
Show answer Hide answer
fun cleanLengths(xs: List<String?>) = xs.filterNotNull().map { it.length }

filterNotNull both removes the nulls and changes the element type to non-null String, so .map { it.length } needs no safe call. (xs.mapNotNull { it?.length } is a one-pass alternative.)

14. Safe on a null receiver

isNullOrBlank() can be called on a null. Without a safe call, implement isMissing returning true when a string is null or blank.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun missing() { Assert.assertTrue(isMissing(null)) Assert.assertTrue(isMissing(" ")) Assert.assertFalse(isMissing("Ada")) } } //sampleStart fun isMissing(s: String?): Boolean = false // TODO: true when null or blank — no ?. needed //sampleEnd
Show answer Hide answer
fun isMissing(s: String?) = s.isNullOrBlank()

isNullOrBlank is an extension on String?, so the null check lives inside it — that’s why no safe call is needed.


Back to the lesson, Kotlin’s Billion-Dollar Fix, or on to the next one: collections.

Comments