We have two loose threads to tie off, and they are both among the best things about Kotlin.
The first is null. We met it briefly in Chapter 2 and bumped into it again in Chapter 12, when we saw that looking up a missing key in a map hands back null — Kotlin's value for "nothing here." We have been tiptoeing around it ever since. In this chapter we face it properly and learn the tools that make handling "nothing" safe and even pleasant.
The second is the lambda — those little { } blocks we glimpsed in Chapter 12, when filter { it > 0 } plucked the living enemies out of a list in a single line. A lambda is a small piece of code you can store in a variable and pass around like any other value, and it powers a whole family of beautifully concise collection operations.
These are the last two pieces of core Kotlin. After this chapter you will have the complete language for everything we do in the rest of the book.
As a quick reality check: I am not suggesting you have learned the entire Kotlin language! Kotlin is known for its huge expressiveness and there are actually many ways to do the same thing. In this book I am showing you what I think is the right way to do everything for the games we build but Kotlin has many, many more idioms, features, and keywords. My other Kotlin book, Android Programming for Beginners 4th Edition, is a more thorough "Kotlin" book but it is less visual/gamey, dare I say, fun, than this book.
In this chapter, we will:
- Understand why
nullexists and the trouble it has historically caused. - Use nullable types, the safe call
?., and the Elvis operator?:. - Know what
!!does and why to treat it with caution. - Understand what a lambda is and how to write one.
- Pass a lambda into a function as an argument.
- Use lambdas with
filter,map, and friends to transform collections in a line. - Try an optional AI exercise on both topics.
Let's start by staring down null.
Null Safety
In many programming languages, any variable that holds an object can secretly also hold null — nothing at all. The trouble comes when you forget, and try to use that nothing. You ask a variable for its length, or tell it to take damage, and it is empty. The program crashes. This single mistake has been so common, in so many languages, for so many years, that the man who invented the null reference, Tony Hoare, famously called it his "billion-dollar mistake."
Kotlin's answer is elegant: it makes nullability part of the type itself, and refuses to let you ignore it. By default, a Kotlin variable can never be null:
var playerName: String = "Alex"
playerName = null // ERROR — the compiler refuses to compile this
A normal String is guaranteed to hold actual text, always. If you want a variable that is allowed to hold nothing, you must say so explicitly, by adding a ? to its type — exactly the question mark we first noticed on Bundle? in Chapter 1:
var currentTarget: String? = "Goblin" // The ? means this may be null
currentTarget = null // Now this is allowed
That ? changes everything. It splits Kotlin's types into two camps: non-null types that can never trip you up, and nullable types (String?, Int?, and so on) that might be empty — and which Kotlin will not let you use carelessly.
The Safe Call: ?.
Once a value is nullable, Kotlin stops you from using it directly. This will not compile:
val length = currentTarget.length // ERROR — currentTarget might be null!
Kotlin is protecting you. Because currentTarget might be null, asking for its .length might be asking nothing for its length, which is the very crash we want to avoid. The compiler forces you to deal with the possibility.
The cleanest way to deal with it is the safe call operator, ?.:
val length = currentTarget?.length
The ?. means "if the thing on the left is not null, carry on and get its length; if it is null, stop and just produce null." So if currentTarget holds "Goblin", length becomes 6. If currentTarget is null, length becomes null too — no crash, no drama. The safe call lets you reach into a nullable value without ever risking the explosion.
The Elvis Operator: ?:
A safe call often leaves you with a nullable result, and usually you want a sensible fallback instead of null. That is the job of the Elvis operator, ?:. (Tilt your head and look at ?: — it is said to resemble Elvis Presley's hair and eyes. Programmers name things strangely.)
The Elvis operator means "use the value on the left, but if that is null, use the value on the right instead":
val name: String? = currentTarget
val displayName = name ?: "No target"
If name holds text, displayName gets that text. If name is null, displayName falls back to "No target". You will use this constantly. Remember the map lookups from Chapter 12 that returned null for a missing key? The Elvis operator is the graceful fix we promised:
val arrowCount = inventory["arrows"] ?: 0 // The count, or 0 if we have none
That single line says "give me the arrow count, and if there is no arrows entry at all, treat it as zero." Clean, safe, and readable. This is the idiom you will reach for whenever a map might not have the key you ask for.
The Non-Null Assertion: !!
There is one more tool, and it comes with a warning. Sometimes you know a value cannot possibly be null, even though the compiler cannot prove it. The non-null assertion !! lets you override the compiler and insist:
val length = currentTarget!!.length
!! means "trust me, this is not null — proceed as if it definitely has a value." And if you are wrong — if the value really was null — your app crashes immediately, with the very NullPointerException that all this machinery exists to prevent.
So !! throws away Kotlin's main safety feature. Use it sparingly, and only when you are genuinely certain. Most of the time, a safe call ?. or an Elvis ?: expresses what you mean more safely. When you find yourself reaching for !!, pause and ask whether one of the gentler tools would do instead.
Checking for null Directly
Finally, the most straightforward approach of all: just check with an if. Kotlin is clever enough to notice the check and relax its rules inside the block:
if (currentTarget != null) {
// In here, Kotlin KNOWS currentTarget isn't null,
// so you can use it normally — no ?. needed
println(currentTarget.length)
}
This is called a smart cast: because you have proven inside the if that the value is not null, Kotlin lets you treat it as a plain, non-null value for the rest of that block. It is the most readable choice when you have several things to do with the value once you know it is there.
Between ?., ?:, !!, and a plain null check, you can handle "nothing" in whatever way reads most clearly — and Kotlin guarantees you will never forget to handle it, because it simply will not compile until you do. That guarantee is worth a great deal. Whole categories of crash that plague other languages just do not happen in well-written Kotlin.
Lambdas
Now to that second thread. Back in Chapter 8 you learned to write functions — named blocks of reusable code. A lambda is a function with no name, written so compactly that you can store it in a variable or hand it to another function as easily as you would pass a number.
Here is the simplest possible lambda, stored in a variable:
val sayHello = { println("Hello!") }
sayHello() // Prints: Hello!
In the preceding code, everything between the braces { } is the lambda — a little parcel of code. We store that parcel in sayHello, and later we run it by writing sayHello(), just like calling a function. The difference from a normal function is that we never wrote fun or gave it a name; we treated the code itself as a value and put it in a box.
A lambda can take parameters too. They go at the start, before an arrow ->, with the body after:
val add = { a: Int, b: Int -> a + b }
val total = add(5, 3) // 8
The part before the -> lists the inputs (a and b); the part after is what the lambda does with them. The last line of a lambda is automatically its result, so this lambda hands back a + b. If you squint, { a: Int, b: Int -> a + b } is just a nameless version of the add function we wrote in Chapter 8.
Passing a Lambda to a Function
So far this looks like a quirky way to write a function. The real power appears when you pass a lambda into another function. A function that accepts another function (or lambda) as a parameter is called a higher-order function, and it lets us write code whose behavior is filled in by the caller.
Here is a function that does something a number of times, where what it does is supplied as a lambda:
fun repeatAction(times: Int, action: () -> Unit) {
for (i in 0 until times) {
action()
}
}
repeatAction(3) {
println("Spawning an enemy!")
}
In the preceding code, the parameter action: () -> Unit is a function-shaped parameter. The type () -> Unit reads as "a function that takes no inputs and returns nothing" — the arrow -> again, describing a function's shape. Inside repeatAction, we run that supplied lambda with action(), once per loop. When we call repeatAction(3) { ... }, the braces are the lambda we are handing in, and the message prints three times.
Notice where the lambda sits: outside the parentheses, after repeatAction(3). This is a Kotlin convenience called the trailing lambda: when a lambda is the last argument to a function, you are allowed to move it outside the parentheses, which reads far more naturally. It looks almost like a built-in language construct rather than a function call — and it is exactly why so much Kotlin code has those trailing { } blocks.
The it Shorthand
One last piece of lambda syntax, and it is the one that explains the mysterious code from Chapter 12. When a lambda takes exactly one parameter, Kotlin lets you skip naming it and refer to it as it:
val isPositive = { n: Int -> n > 0 } // naming the parameter 'n'
val isPositive = { it > 0 } // using the built-in 'it' instead
Both lambdas do the same thing. The second just leans on it as the automatic name for "the one thing passed in." It is a small shortcut, but it is everywhere in Kotlin, and now you know what that lonely it means whenever you see it.
Lambdas Meet Collections
Here is the payoff. Back in Chapter 12 we glimpsed filter and map and promised an explanation. With lambdas in hand, that code is no longer mysterious — it is just a collection running a lambda you give it, once per element.
filter keeps only the elements for which your lambda returns true:
val healths = listOf(30, 0, 75, 0, 10)
val alive = healths.filter { it > 0 } // [30, 75, 10]
filter walks the list, and for each element it runs your lambda — { it > 0 } — handing the element in as it. Where the lambda returns true, the element is kept; where it returns false, it is dropped. One line replaces the whole loop-and-add we wrote out the long way in Chapter 12.
map transforms every element into something new (no relation to the map collection — the names just clash):
val doubled = healths.map { it * 2 } // [60, 0, 150, 0, 20]
Here the lambda { it * 2 } runs on each element, and map collects the results into a new list.
There are many more, and they read like plain English once you know the pattern. A few you will reach for often:
val deadCount = healths.count { it == 0 } // 2 — how many match
val anyAlive = healths.any { it > 0 } // true — is at least one a match?
healths.forEach { println(it) } // run a lambda on every element
count tallies how many elements match, any answers whether at least one matches, and forEach simply runs your lambda on each element — a tidy alternative to a for loop when all you want to do is act on every item. Each takes a lambda, each does one clear job, and each replaces several lines of looping with one readable line.
A word of balance: these operations are wonderful for clarity, but the plain for loops you already know are never wrong, and sometimes clearer. Use whichever reads better for the job in front of you. In tight game loops we will often still prefer an explicit loop — but for everyday list-wrangling, filter, map, and friends are a joy.
Summary
You have closed the last two gaps in core Kotlin. On nullability: a ? marks a type that may hold nothing, the safe call ?. reaches into it without crashing, the Elvis operator ?: supplies a fallback when it is null, !! overrides the compiler at your own risk, and a plain null check lets Kotlin smart-cast the value to non-null inside the block. Together they mean the billion-dollar mistake simply cannot sneak into your code unnoticed. On lambdas: a lambda is a nameless block of code you can store and pass around, written with { params -> body }, often shortened with it, and frequently handed to higher-order functions as a trailing block — which is exactly what powers filter, map, count, and the rest of Kotlin's expressive collection operations.
That completes Act 2, and very nearly completes the Kotlin language as this book uses it. You now have variables, control flow, loops, functions, the full family of collections, null safety, and lambdas. In the next chapter we put the newest tools to work in the Advanced Spawner: a project that uses lambdas as callbacks — running a piece of code you supply when something happens — and leans on nullability to handle game objects that may or may not exist. After that, Act 3 opens the door to object-oriented programming, where we finally learn to bundle data and behavior together into classes — and rebuild that messy parallel-list particle swarm into something clean.
AI Exercise (Optional)
Nullability and lambdas are perfect for an AI conversation, because they are where Kotlin differs most from other languages, and an AI can show you the same idea from several angles. As always, optional.
Open your AI chatbot and paste in this prompt:
"I am a beginner learning Kotlin. I have just learned about null safety (nullable types with
?, the safe call?., the Elvis operator?:,!!, and smart casts with an if null-check) and about lambdas (the{ params -> body }syntax, theitshorthand, passing a lambda to a higher-order function, and collection operations like filter, map, count, and any). Please do two things. First, show me a single line that looks up a key in aMap<String, Int>and falls back to 0 if the key is missing, and explain exactly which operator does the fallback. Second, take this list —listOf(40, 0, 90, 25, 0)representing enemy health — and show me, in separate one-line examples, how to get only the living enemies, how many are dead, whether any are alive, and a new list with every health doubled. Explain whatitrefers to in each one."
This prompt deliberately revisits the exact situations from this chapter and Chapter 12 — the map fallback and the four collection operations — so you can check the AI's answers against your own understanding directly.
When the answer comes back, read each one-liner and say out loud what it stands for and what the operation returns. Does the AI correctly identify the Elvis operator ?: as the thing doing the fallback? For the collection operations, predict each result before you trust the AI's. If any explanation is thin, ask it to rewrite one of the one-liners as a plain for loop so you can see that the lambda version really is just a shorter way of writing the loop you already know. Connecting the new shortcut back to the familiar loop is the fastest way to make it stick.