Access, search, and filter collections
These operations answer three different questions:
- Access: Which element is at this position?
- Search: Where is the first element matching this rule?
- Filter: Give me every element matching this rule.
Safe element access
| Need | Throwing operation | Safer nullable operation |
|---|---|---|
| First element | first() | firstOrNull() |
| Last element | last() | lastOrNull() |
| Exactly one element | single() | singleOrNull() |
| Element at index | list[index] / elementAt() | getOrNull() / elementAtOrNull() |
fun main() {
val names = listOf("Rakesh", "Anu")
val empty = emptyList<String>()
println(names.first()) // Rakesh
println(names.getOrNull(10)) // null
println(empty.firstOrNull()) // null
println(empty.singleOrNull()) // null
val failure = runCatching { empty.first() }
println(failure.exceptionOrNull()!!::class.simpleName)
}
Use a throwing operation when absence means a broken invariant. Use the nullable form when absence is expected business behavior. Do not automatically replace every exception with null; that can hide corrupted state.
Search by value, position, or predicate
fun main() {
val skills = listOf("Java", "Kotlin", "Compose", "Kotlin")
println(skills.indexOf("Kotlin")) // 1
println(skills.lastIndexOf("Kotlin")) // 3
println(skills.indexOfFirst { it.length > 6 }) // 2
println(skills.indexOfLast { it.startsWith("K") }) // 3
println(skills.find { it.startsWith("C") }) // Compose
println(skills.find { it.startsWith("X") }) // null
}
find(predicate) is an alias for firstOrNull(predicate). Index-search operations return -1 when nothing matches; element-search operations commonly return null.
Fallback and arbitrary retrieval
fun main() {
val values = listOf("A", "B", "C")
println(values.elementAtOrElse(10) { index -> "fallback-for-$index" })
println(values.random())
println(values.randomOrNull())
println(emptyList<String>().randomOrNull()) // null
}
random() throws on empty input; randomOrNull() returns null. Both are nondeterministic for a non-empty input unless supplied a controlled Random. Inject a seeded Random in repeatable tests. Do not use ordinary pseudo-random collection selection for security-sensitive choices.
Filter families
| Operator | What remains |
|---|---|
filter | Matching values |
filterNot | Non-matching values |
filterIndexed | Values selected using index and value |
filterNotNull | Non-null values, with nullable type removed |
filterIsInstance<T> | Runtime values of type T |
fun main() {
val values: List<Any?> = listOf(10, 20L, 30.5f, "Android", null, 40)
val ints = values.filterIsInstance<Int>()
val numbers = values.filterIsInstance<Number>()
val total = numbers.sumOf { it.toDouble() }
println(ints) // [10, 40]
println(numbers) // [10, 20, 30.5, 40]
println(total) // 100.5
}
filterIsInstance<Int>() keeps only Int. It excludes Long, Float, and Double. Use Number when the domain genuinely accepts different numeric representations, then convert deliberately before arithmetic.
Predict before revealing
val words = listOf("one", "three", "two")
println(words.first { it.length == 3 })
println(words.filter { it.length == 3 })
Reveal the output and explanation
first stops at one. filter examines the complete list and returns [one, two].
Android scenario
When reading a selected item from UI state, firstOrNull may correctly represent “nothing selected.” When reading a required database migration record, returning null may hide a violated invariant. The operation is a product decision, not merely a syntax preference.
Interview questions
- Why can
firstOrNull()be safer but still logically wrong? - What does
indexOfFirstreturn when no element matches? - Why does
filterIsInstance<Int>()exclude aLong? - When should
single()be preferred overfirst()?