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.
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.
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.
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.
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.
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.
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.
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.
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).
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?
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!!.lengthIf 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.
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?>.
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.
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