Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
153: Null Safety in Collections
We've talked about nullable types, but things get a bit more nuanced when you throw collections into the mix. The biggest point of confusion for most developers—and it took me a while to wrap my head around it too—is the difference between a nullable list and a list containing nullable elements. They look similar, but they require completely different handling strategies.
Handling the "Maybe" Guest List
Let's build a small RSVP processor. Imagine we're managing a guest list for a tech meetup. Sometimes the entire list is missing from the database (the list itself is null), and sometimes a guest signed up but didn't provide their name (an element in the list is null). This gives us a type like List<String?>?.
val guestList: List<String?>? = listOf("Alice", null, "Bob", null, "Charlie")
See that? The outer ? means the whole list might be null. The inner ? means the names inside could be null. If I want to print the names, I can't just loop through them blindly. I have to handle both layers of "maybe."
Where I tripped over nullable elements
I wanted to calculate the total number of characters across all provided names to estimate how much ink I'd need for name tags. At first, I tried this:
// My first (wrong) attempt
val totalChars = guestList?.map { it.length }?.sum()
The compiler immediately yelled at me. Why? Because it inside that map refers to a String?. You can't call .length on a nullable string without a safe call or a null check. I thought, "Easy, I'll just use a safe call," and did this:
val totalChars = guestList?.map { it?.length }?.sum()
This actually compiles, but it's not what I wanted. it?.length returns an Int?. Now my map has produced a List<Int?>. While Kotlin's sum() is clever enough to handle some of this, I'm now dealing with a collection of nullable integers, which is a recipe for bugs later in the pipeline if I need to do more complex math.
Filtering the noise out
The cleaner way to handle this is to strip out the nulls entirely before I start transforming the data. Kotlin provides a fantastic extension function for this: filterNotNull(). This function doesn't just remove the nulls; it actually changes the type of the collection from List<T?> to List<T>.
Here is how I actually solved it:
val guestList: List<String?>? = listOf("Alice", null, "Bob", null, "Charlie")
val totalChars = guestList
?.filterNotNull() // This transforms List<String?> to List<String>
?.map { it.length } // Now 'it' is definitely a String, no ? needed
?.sum() ?: 0 // Handle the case where the entire guestList was null
println("Total characters: $totalChars")
By using filterNotNull(), I've pushed the null-handling to the very beginning of the chain. This makes the rest of the logic—the mapping and the summing—much cleaner because I'm working with guaranteed values. It's a pattern I use constantly: filter the junk out early, then process the clean data.
📋 Practical Task
Exercise: Cleaning the Product Inventory
You are building a warehouse tool. You have a list of product weights, but the data is messy. Some weights are missing (null), and the entire inventory list might be null if the database connection fails.
Your Task:
Write a function called calculateTotalWeight that takes a List<Double?>? as an argument. The function should:
- Return
0.0if the list itself is null. - Filter out all null weights within the list.
- Calculate the sum of the remaining weights.
- Return the final sum as a
Double.
fun calculateTotalWeight(weights: List<Double?>?): Double {
// Your code here
}
// Test cases to verify your logic:
// calculateTotalWeight(listOf(1.2, null, 3.4, null, 0.4)) should return 5.0
// calculateTotalWeight(null) should return 0.0
// calculateTotalWeight(listOf(null, null)) should return 0.0
There are no comments for now.