Skip to main content

Collection operations through practical scenarios

The important skill is not remembering names. It is predicting output, duplicate behavior, failure on empty input, ordering, and allocation.

Transformation: map, flatMap, and flatten​

val names = users.map { user -> user.name }
Open in Kotlin Playground

map produces exactly one output for each input.

val orderItems = orders.flatMap { order -> order.items }
Open in Kotlin Playground

flatMap maps each order to a list, then flattens one level. If nested lists already exist, flatten() only flattens:

val nested = listOf(listOf(1, 2), listOf(3))
println(nested.flatten()) // [1, 2, 3]
Open in Kotlin Playground

Selection: filter, partition, and mapNotNull​

val (valid, invalid) = inputs.partition(::isValid)
Open in Kotlin Playground

partition evaluates each element and returns two lists. Use filter when only one side matters.

val ids = rawIds.mapNotNull(UserId::parse)
Open in Kotlin Playground

mapNotNull transforms and removes null results. This is useful only if discarding invalid entries is an intentional policy; otherwise return validation errors.

Accumulation: reduce versus fold​

val total = prices.fold(Money.zero("INR")) { sum, price -> sum + price }
Open in Kotlin Playground

fold starts with an explicit initial accumulator and works for empty collections. reduce uses the first element as the initial accumulator and throws on empty input.

emptyList<Int>().fold(0, Int::plus) // 0
// emptyList<Int>().reduce(Int::plus) // throws
Open in Kotlin Playground

Use fold when a meaningful identity exists or the result type differs from element type.

Indexing: associateBy, associateWith, and groupBy​

data class User(val id: Int, val team: String, val name: String)

val byId: Map<Int, User> = users.associateBy(User::id)
val byTeam: Map<String, List<User>> = users.groupBy(User::team)
Open in Kotlin Playground
  • associateBy stores one value per key; later duplicates overwrite earlier ones.
  • groupBy keeps a list of all values for each key and allocates those lists.
  • associateWith uses each original element as a key and computes its value.

Duplicate-key trap​

val users = listOf(
User(1, "Android", "Old"),
User(1, "Android", "New"),
)
println(users.associateBy(User::id)[1]?.name) // New
Open in Kotlin Playground

If duplicate IDs indicate corrupt data, silently keeping the last is wrong. Validate duplicates before indexing.

Predicate operations short-circuit​

OperatorMeaning on elementsEmpty collection result
any(predicate)At least one matchesfalse
all(predicate)Every element matchestrue
none(predicate)No element matchestrue

all on an empty collection is true by vacuous truth. That can surprise validation code:

val allApproved = requests.all(Request::isApproved)
Open in Kotlin Playground

If “at least one request must exist” is also required, check requests.isNotEmpty() && requests.all(...).

Pairing: zip and unzip​

val labels = listOf("Name", "Age")
val values = listOf("Rakesh", "35", "Ignored")
println(labels.zip(values)) // [(Name, Rakesh), (Age, 35)]
Open in Kotlin Playground

zip stops at the shorter collection. It does not report missing values. Use key-based association when position is not a guaranteed contract.

val pairs = listOf("U-1" to "Rakesh", "U-2" to "Anu")
val (ids, names) = pairs.unzip()
Open in Kotlin Playground

Slicing from the start and end​

There are two slicing families:

FamilyStart sideEnd sideStops because of
Fixed counttake(n), drop(n)takeLast(n), dropLast(n)Number of elements
PredicatetakeWhile, dropWhiletakeLastWhile, dropLastWhileFirst predicate failure from that side

Fixed-count slicing​

val values = listOf(10, 20, 30, 40, 50)

println(values.take(2)) // [10, 20]
println(values.drop(2)) // [30, 40, 50]
println(values.takeLast(2)) // [40, 50]
println(values.dropLast(2)) // [10, 20, 30]
Open in Kotlin Playground

If n exceeds the collection size, take/takeLast return all elements and drop/dropLast return an empty list. A negative n throws IllegalArgumentException.

Predicate slicing: the complete four-operation comparison​

These operate on a consecutive boundary, not every matching element:

val values = listOf(2, 4, 1, 6, 8)
println(values.takeWhile { it % 2 == 0 }) // [2, 4]
println(values.dropWhile { it % 2 == 0 }) // [1, 6, 8]
println(values.takeLastWhile { it % 2 == 0 }) // [6, 8]
println(values.dropLastWhile { it % 2 == 0 }) // [2, 4, 1]
Open in Kotlin Playground

Read the output from the boundary:

  • takeWhile starts at the beginning and keeps matching values until 1 fails.
  • dropWhile starts at the beginning and removes matching values until 1 fails; everything after that remains, even if it matches.
  • takeLastWhile starts testing at the end and keeps the matching suffix 6, 8. The returned elements remain in their original order.
  • dropLastWhile removes that matching suffix and keeps the prefix 2, 4, 1.

Use filter if you mean “all matching values anywhere.” For this input, filter returns [2, 4, 6, 8].

Common mistake

The word Last changes which boundary is inspected; it does not reverse the returned list. takeLastWhile scans the suffix condition from the end but returns the retained suffix in original encounter order.

Uniqueness: distinct and distinctBy​

val newestFirst = messages.sortedByDescending(Message::createdAt)
val latestPerSender = newestFirst.distinctBy(Message::senderId)
Open in Kotlin Playground

distinctBy keeps the first item encountered for each selected key. Therefore sorting/order before it is part of the result contract.

Batching and sliding analysis​

val pages = (1..10).toList().chunked(3)
// [[1,2,3], [4,5,6], [7,8,9], [10]]

val movingPairs = listOf(10, 20, 30, 40).windowed(size = 2)
// [[10,20], [20,30], [30,40]]
Open in Kotlin Playground
  • chunked(n) creates non-overlapping batches, useful for bounded API requests.
  • windowed(n) creates sliding windows, useful for moving comparisons; it can allocate heavily on large inputs.
Open in Kotlin Playground

Collections versus sequences​

val result = hugeList
.asSequence()
.filter(::isEligible)
.map(::toUi)
.take(10)
.toList()
Open in Kotlin Playground

Collections usually execute each stage eagerly and create intermediate results. A sequence processes elements lazily through the pipeline and can stop early. Sequences are not always faster: small inputs/simple pipelines may pay iterator/lambda overhead without saving meaningful allocation. Measure hot paths.

runCatching is not a collection operator​

It produces Result<T> from a block:

val parsed: Result<Int> = runCatching { raw.toInt() }
Open in Kotlin Playground

Be careful in suspend code: broad catching mechanisms can capture CancellationException depending on usage. Do not turn coroutine cancellation into an ordinary failure.

Android scenarios​

API batch limit​

pendingIds.chunked(100).forEach { batch ->
api.uploadBatch(batch)
}
Open in Kotlin Playground

Decide whether batches run sequentially or with bounded concurrency, what happens after partial failure, and whether the API is idempotent. chunked solves grouping, not distributed reliability.

Rendering grouped settings​

val sections = settings.groupBy(Setting::category)
Open in Kotlin Playground

If order matters, ensure input/map implementation and category ordering are explicit. Do not rely on incidental implementation order as a business contract.

Deduplicating network data​

associateBy(id) may hide server duplicates. Record/alert when duplicates violate the contract before selecting a winner.

Interview questions​

Why does reduce fail on empty input while fold does not?

Reduce needs the first element as its initial accumulator. Fold receives an explicit initial value, so it can return that value for empty input.

What happens when associateBy sees duplicate keys?

The later value replaces the earlier value for that key. Whether that is acceptable is a domain decision.

How is takeWhile different from filter?

takeWhile stops at the first non-matching element; filter examines the whole input and retains every match.

Does takeLastWhile reverse the result?

No. It tests the consecutive suffix from the end, but returns the retained elements in their original order.

Exercises​

  1. Predict all outputs before running the Playground example.
  2. Build a map that rejects duplicate IDs instead of silently overwriting.
  3. Calculate a moving three-sample average using windowed.
  4. Compare eager and sequence pipelines with logging and take(2).
  5. Design partial-failure handling for chunked(100) uploads.
  6. For [2, 4, 1, 6, 8], predict all four ...While slicing results before running the example.