Collections in depth: views and mutability

Scala · Practical · 14 min

Choosing collections well matters for performance and memory in real workloads.

The collections library is a hierarchy: Iterable at the top, then Seq (ordered, with IndexedSeq like Vector/ArraySeq and LinearSeq like List), Set and Map. Code against the most general type that fits (Seq[A], Iterable[A]), and pick the concrete one for its performance:

  • List: O(1) prepend/head/tail, O(n) index and append.
  • Vector: effectively constant index, update, append and prepend.
  • ArraySeq/Array: compact and fastest for primitives; Array is mutable.
  • HashMap/HashSet default; TreeMap/TreeSet keep keys sorted; ListMap keeps insertion order but is slow (the default immutable Map keeps insertion order only up to 4 entries).

Chained operations like xs.map(f).filter(p).take(10) build an intermediate collection at each step. A view (xs.view.map(f).filter(p).take(10).toList) fuses them lazily and computes only what's needed. LazyList is a lazily evaluated, memoised sequence that can be infinite; Iterator is a one-shot lazy stream.

Mutable collections (scala.collection.mutable.ArrayBuffer, HashMap, ListBuffer) are fine for local, performance-sensitive building; keep them private and expose immutable results (buf.toList). The scala.jdk.CollectionConverters import adds .asScala/.asJava for Java interop.

Example

import scala.collection.mutable

val evens = LazyList.from(1).filter(_ % 2 == 0)
println(evens.take(5).toList)                   // List(2, 4, 6, 8, 10)

var calls = 0
val r = (1 to 1_000_000).view
  .map { x => calls += 1; x * x }
  .filter(_ % 7 == 0)
  .take(3)
  .toList
println(s"$r after $calls calls")               // List(49, 196, 441) after 21 calls

val buf = mutable.ArrayBuffer.empty[Int]
for i <- 1 to 5 do buf += i * 10
val frozen: Vector[Int] = buf.toVector          // expose immutable
println(frozen)                                 // Vector(10, 20, 30, 40, 50)

val counts = mutable.Map.empty[Char, Int].withDefaultValue(0)
"mississippi".foreach(c => counts(c) += 1)
println(counts.toSeq.sortBy(-_._2).head)        // (i,4) or (s,4)

val sorted = collection.immutable.TreeMap(3 -> "c", 1 -> "a", 2 -> "b")
println(sorted.keys.toList)                     // List(1, 2, 3)

Remember

  • Code against Seq/Iterable; choose List, Vector or ArraySeq for performance
  • Views and LazyList avoid intermediate collections
  • Mutable collections for local building, immutable results outward
  • TreeMap for sorted keys; default Map order is not guaranteed

Watch out for

  • Relying on the iteration order of a default immutable Map or Set with more than four elements.

Check yourself

  1. Why use .view before a chain of map/filter/take?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.scala-lang.org/overviews/collections-2.13/performance-characteristics.html