Collections & Sequences
Kotlin's collections are not new data structures layered on top of the JVM — they are a new set of interfaces over the old ones. Behind List<T>, MutableList<T> and ArrayList<T> usually sits the very same java.util.ArrayList, and the difference between the first two exists only at compile time: List simply does not declare the mutating methods. Hence the headline conclusion of the topic, probed in every other interview: read-only is a contract, not a guarantee of immutability. The List you handed out will happily change if someone else keeps a MutableList reference to the same object.
The second dividing line is when an operator chain runs. Over a plain Iterable it is eager: every operator walks the whole collection and allocates a new intermediate list, so list.map { }.filter { } means two lists and two passes over the data. A Sequence inverts the evaluation order: the operators merely assemble a pipeline of wrappers, nothing executes until a terminal operation, and then the elements travel through the entire chain one at a time, vertically. That is where the win comes from (no intermediate lists, first() cuts the work short at the first match) — and the trap too (on a small collection the iterator machinery costs more than the short-lived lists, so a Sequence is simply slower). The third axis is the JVM representation: IntArray compiles to exactly int[], while Array<Int> compiles to Integer[], where every element is a separately boxed object. The layer-by-layer breakdown is below.
Topic map
- Read-only and mutable collections — why a
Listis not immutable, what happens to it at the Java boundary, and how to hand out a genuine snapshot. - Collection transformations —
map,filterandflatMap, their effect on the result size, and the price of every operator materialising a new list. - fold versus reduce — an explicit seed versus the first element, behaviour on an empty collection, and changing the accumulator type.
- Grouping and associating —
groupBy, which keeps every element,associateBy, whose last key silently overwrites the earlier one, andgroupingBywith no intermediate lists. - Sequences and lazy evaluation — the pipeline of wrappers, terminal operations, short-circuiting, and the exact answer to when laziness pays off and when it hurts.
- Arrays and primitive arrays —
Array<T>versusIntArrayon the JVM, array invariance in Kotlin, content comparison, andvararg.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
Treating List as an immutable runtime type | It is a read-only view: the same object changes through any retained MutableList reference |
Believing a List hands you a defensive copy of the elements | There is no copy — List and MutableList are one object; only toList() takes a snapshot |
Thinking read-only is enforced by UnsupportedOperationException | The mutating methods are simply absent from the List API — the compiler forbids them, not the runtime |
Forgetting that both erase to java.util.List on the JVM | Java code will happily call add() on your List, and the Kotlin side sees the change |
Calling map when the transform returns a collection | You get a List<List<T>> instead of a flat list — this is flatMap's job |
Expecting flatMap to preserve the source size | The result size is arbitrary, and it does not deduplicate or drop nulls along the way |
Assuming the compiler fuses a List chain into a single pass | There is no fusion: map{}.filter{} is two passes and two allocated lists |
Reaching for reduce when the result type differs from the element type | reduce's accumulator has the element type — only fold can change the type |
Forgetting that reduce throws UnsupportedOperationException on an empty collection | A production crash on an empty list; a seeded fold or reduceOrNull survive it |
Guarding with isEmpty() instead of seeding the accumulator | A redundant branch where fold's initial value already is the result for the empty case |
Using associateBy on a non-unique key | The last element silently overwrites the earlier one — data is lost without a single exception |
Expecting associateBy to fail loudly on a duplicate key | It never fails; guaranteeing key uniqueness is your job |
Thinking groupBy keeps only one element per key | groupBy returns a Map<K, List<V>> and keeps every element of the group |
Thinking asSequence() alone starts the work | Without a terminal operation not one lambda runs — the chain is merely assembled |
Taking a Sequence for a parallel construct | It is a lazy single-threaded pipeline; it uses no threads and no extra cores |
Wrapping a three-element list in asSequence() for speed | On small data a Sequence is slower: its operators are not inlined, each step is a virtual call |
Trying to add to an Array<T> | An array's size is fixed at creation — List grows, an array does not |
Assuming IntArray boxes its elements the way Array<Int> does | IntArray is an int[] with no wrappers; Array<Int> is an Integer[] with an object per element |
Comparing arrays with == | That compares references; content is compared with contentEquals / contentDeepEquals |
Interview relevance
The topic looks junior-level and works as a filter: nearly everyone knows what map does, and almost nobody can say how many lists map{}.filter{}.first() allocates, or why that number is zero on a Sequence. What the interviewer probes here is not your operator vocabulary but your execution model — what physically sits behind a List, at which moment a lambda fires, and exactly where data can quietly go missing. Three questions pull out almost all of it: "how does List differ from MutableList at runtime", "why does reduce crash on an empty list", and "when does a Sequence pay off".
Typical checks:
- That the
List/MutableListsplit is a compiler contract, and that read-only does not mean immutable. - The difference between
mapandflatMap, and that every operator over anIterablematerialises a new list. foldversusreduce: the seed, the accumulator type, and the behaviour on an empty collection.groupByversusassociateBy, and the silent loss of elements on a duplicate key.- The evaluation order of a
Sequence, the role of the terminal operation, and short-circuiting. - When a lazy chain pays off and when it loses to the eager one.
Array<T>versusIntArrayon the JVM, and why arrays are not compared with==.
Common wrong answer: "List is the immutable collection and MutableList is the mutable one." It sounds confident and leads straight to a bug: the candidate hands out a List obtained by plain assignment from a MutableList, assumes the data is protected — and ships a collection that mutates under the caller. In reality List is a read-only interface over the same object, and only a copy (toList()) gives you a true snapshot. The second classic failure is "a Sequence is always faster": on a collection of a dozen elements the lazy chain loses, because the Iterable operators are inlined while every Sequence stage is a wrapper object with virtual calls per element.