Collections & Sequences
The standard-library collection API — map, filter and flatMap transformations, eager List versus lazy Sequence evaluation, read-only versus mutable collections at runtime, fold versus reduce, groupBy versus associateBy, and arrays including the primitive IntArray family.
9 questions
JuniorTheoryVery commonHow do Array<T>, List<T> and IntArray differ from one another in Kotlin?
How do Array<T>, List<T> and IntArray differ from one another in Kotlin?
Array<T> is a fixed-size JVM object array whose slots can be reassigned. List<T> is an interface, normally backed by an ArrayList, that can grow. IntArray is a fixed-size primitive int[], so none of its elements are boxed.
Common mistakes
- ✗Trying to
addto anArray<T>, whose size is fixed at creation - ✗Assuming
IntArrayboxes its elements the wayArray<Int>does - ✗Reaching for
Array<Int>in hot numeric code whereIntArraybelongs
Follow-up questions
- →When would you deliberately pick an
Array<T>over aList<T>? - →What does
toIntArray()cost when you call it on aList<Int>?
JuniorTheoryVery commonWhat is the difference between map and flatMap on a Kotlin collection?
What is the difference between map and flatMap on a Kotlin collection?
map applies the transform to each element and hands back one result per element, so the size is unchanged. flatMap expects a collection back from the transform and concatenates all of them into one flat list.
Common mistakes
- ✗Calling
mapwhen the transform returns a collection, ending up with a list of lists - ✗Expecting
flatMapto preserve the size of the source list - ✗Thinking
flatMapdeduplicates or drops nulls while it flattens
Follow-up questions
- →How would you flatten an existing
List<List<T>>when there is no transform to apply? - →What does
mapNotNullgive you over amapfollowed byfilterNotNull?
JuniorTheoryCommonWhat is the difference between List and MutableList at runtime?
What is the difference between List and MutableList at runtime?
None: the distinction is a compile-time contract. Both are usually the same java.util.ArrayList, and List is an interface that omits the mutating methods. So a List you hold can still change if someone else keeps a mutable reference to it.
Common mistakes
- ✗Believing a
Listhands you a defensive copy of the elements - ✗Assuming a
Listyou received can never change afterwards - ✗Thinking read-only is enforced by throwing
UnsupportedOperationException
Follow-up questions
- →How do you hand out a list that genuinely cannot change under the caller?
- →Why can a
Listthat came back from Java code still be mutated?
JuniorTheoryCommonHow does a Sequence evaluate its operators compared with a List?
How does a Sequence evaluate its operators compared with a List?
A List chain is eager: every operator runs over the whole collection and allocates a new intermediate list. A Sequence is lazy: nothing runs until a terminal operator, then each element goes through the chain one at a time.
Common mistakes
- ✗Thinking
asSequence()alone starts the work without a terminal operator - ✗Assuming a
Listchain fuses its operators into a single pass - ✗Believing a
Sequenceis a parallel or concurrent construct
Follow-up questions
- →Which terminal operators can stop a
Sequencechain early, and why can they? - →How many intermediate lists does a three-operator
Listchain allocate?
MiddleTheoryCommonWhat is the difference between associateBy and groupBy on a list?
What is the difference between associateBy and groupBy on a list?
groupBy gives back a Map<K, List<V>> and keeps every element under its key. associateBy gives back a Map<K, V> with one value per key, so when two elements share a key the last one silently overwrites the earlier one.
Common mistakes
- ✗Using
associateByon a non-unique key and quietly losing elements - ✗Expecting
associateByto fail loudly when a key repeats - ✗Thinking
groupBykeeps only one element per key
Follow-up questions
- →How does
associatediffer fromassociateByin what its lambda returns? - →Which operator counts the elements per key in a single pass?
MiddleCodeCommonTotal the character lengths of a list of names, returning 0 for an empty list
Total the character lengths of a list of names, returning 0 for an empty list
reduce has no seed: it starts from the first element, throws on an empty collection, and its accumulator has the element type. fold takes an initial value, so it handles the empty case and changes type: fold(0) { acc, n -> acc + n.length }.
Common mistakes
- ✗Reaching for
reducewhen the result type differs from the element type - ✗Forgetting that
reducethrowsUnsupportedOperationExceptionon an empty collection - ✗Guarding with
isEmpty()instead of seeding the accumulator
Follow-up questions
- →When would
runningFoldbe a better fit here thanfold? - →How does
reduceOrNullchange the behaviour on an empty collection?
MiddlePerformanceOccasionalWhen does converting a collection chain to a Sequence actually pay off?
When does converting a collection chain to a Sequence actually pay off?
It pays on large data or a long chain: a Sequence allocates no intermediate lists, and a terminal like first() or take(n) cuts the work short. On a small collection it loses, as the iterator machinery costs more than the short-lived lists.
Common mistakes
- ✗Wrapping a three-element list in
asSequence()and expecting a speedup - ✗Believing a
Sequencespreads the work across threads - ✗Forgetting that a
Listchain allocates a fresh list per operator
Follow-up questions
- →How many intermediate lists does
list.map { }.filter { }.take(5)allocate? - →Would you settle this with the JVM microbenchmark harness
JMH, and why?
SeniorTheoryOccasionalHow do Array<Int>, IntArray and Java's int[] differ on the JVM?
How do Array<Int>, IntArray and Java's int[] differ on the JVM?
IntArray compiles to precisely Java's int[]: a contiguous block of primitives, no wrapper objects. Array<Int> compiles to Integer[], so every element is a separately boxed object with a header and an indirection on each read.
Common mistakes
- ✗Declaring
Array<Int>in hot numeric code and paying for the boxing - ✗Assuming
Array<Int>andIntArrayshare one JVM representation - ✗Expecting the JIT to strip the boxing out of an
Integer[]
Follow-up questions
- →Which stdlib call turns a
List<Int>into anIntArray, and what does it cost? - →Why can a generic function taking
Array<T>not accept anIntArray?
SeniorTheoryOccasionalWhy does map { }.filter { }.first() do far less work on a Sequence than on a List?
Why does map { }.filter { }.first() do far less work on a Sequence than on a List?
On a List each operator runs to completion over every element, so map and filter each fire n times and two intermediate lists are built. On a Sequence the elements flow through one at a time, and first() stops at the first match.
Common mistakes
- ✗Assuming the compiler fuses a
Listchain into a single pass - ✗Expecting
first()to short-circuit the earlier operators of aListchain - ✗Thinking a
Sequencestill materialises the filtered result beforefirst()
Follow-up questions
- →How does the picture change when the terminal operator is
toList()instead? - →At what collection size does
asSequence()stop paying for itself here?