Scala - Collection Conversions (toList, toArray, toMap)

Scala's collection library provides multiple mechanisms for converting between collection types. The most common approach uses explicit conversion methods like `toList`, `toArray`, `toSet`, and...

Key Insights

  • Scala provides a unified conversion API through to[Collection] methods and dedicated conversion methods like toList, toArray, and toMap that work across all collection types
  • Converting between mutable and immutable collections, or between different collection types, involves trade-offs in memory allocation and performance that matter in production systems
  • Map conversions require special handling for key-value pairs, with different strategies needed when converting from sequences versus other collection types

Understanding Scala’s Collection Conversion System

Scala’s collection library provides multiple mechanisms for converting between collection types. The most common approach uses explicit conversion methods like toList, toArray, toSet, and toMap. These methods are available on all collection types through the IterableOnce trait.

val list = List(1, 2, 3, 4, 5)
val array = list.toArray
val set = list.toSet
val vector = list.toVector

println(array.mkString("[", ", ", "]"))  // [1, 2, 3, 4, 5]
println(set)                              // Set(5, 1, 2, 3, 4)
println(vector)                           // Vector(1, 2, 3, 4, 5)

Scala 2.13 introduced the to method with a type parameter for more flexible conversions:

import scala.collection.immutable.ArraySeq

val list = List(1, 2, 3)
val seq = list.to(ArraySeq)
val lazyList = list.to(LazyList)

// Using companion objects
val queue = list.to(scala.collection.immutable.Queue)

Converting to Lists and Arrays

The toList conversion is straightforward and creates an immutable linked list. This operation is efficient when the source is already a sequential collection but requires traversing the entire collection.

val range = 1 to 10
val list = range.toList

val set = Set("apple", "banana", "cherry")
val listFromSet = set.toList  // Order not guaranteed

val map = Map("a" -> 1, "b" -> 2, "c" -> 3)
val listFromMap = map.toList  // List[(String, Int)]
println(listFromMap)          // List((a,1), (b,2), (c,3))

The toArray conversion creates a mutable array, which requires allocating contiguous memory. This is useful for interop with Java libraries or when you need indexed access performance.

val list = List(1, 2, 3, 4, 5)
val array = list.toArray

// Modify the array
array(0) = 100
println(array.toList)  // List(100, 2, 3, 4, 5)

// Type-specific arrays
val strings = List("a", "b", "c")
val stringArray: Array[String] = strings.toArray

// Arrays preserve the element type
val numbers: Array[Int] = List(1, 2, 3).toArray

Performance considerations matter when converting large collections:

// Efficient: Vector to Array (both indexed)
val vector = Vector.fill(100000)(42)
val arr1 = vector.toArray

// Less efficient: List to Array (sequential to indexed)
val bigList = List.fill(100000)(42)
val arr2 = bigList.toArray

// Efficient: Array to List with proper builder
val arraySource = Array.fill(100000)(42)
val list2 = arraySource.toList

Converting to Maps

Converting to maps requires key-value pairs. The most common pattern uses tuples:

val pairs = List(("a", 1), ("b", 2), ("c", 3))
val map = pairs.toMap
println(map)  // Map(a -> 1, b -> 2, c -> 3)

// Duplicate keys: last value wins
val duplicates = List(("a", 1), ("b", 2), ("a", 3))
val mapWithDuplicates = duplicates.toMap
println(mapWithDuplicates)  // Map(a -> 3, b -> 2)

Converting sequences to maps using indexes or custom key functions:

val fruits = List("apple", "banana", "cherry")

// Using zipWithIndex
val indexedMap = fruits.zipWithIndex.map { case (v, k) => (k, v) }.toMap
println(indexedMap)  // Map(0 -> apple, 1 -> banana, 2 -> cherry)

// Using map with custom key function
val lengthMap = fruits.map(f => (f.length, f)).toMap
println(lengthMap)  // Map(5 -> apple, 6 -> banana, cherry)

// Using groupBy for multiple values per key
val grouped = fruits.groupBy(_.length)
println(grouped)  // Map(5 -> List(apple), 6 -> List(banana, cherry))

Converting maps back to other collections:

val map = Map("a" -> 1, "b" -> 2, "c" -> 3)

val keys = map.keys.toList
val values = map.values.toList
val entries = map.toList

// Flatten to sequence
val flatSeq = map.toSeq.flatMap { case (k, v) => Seq(k, v.toString) }
println(flatSeq)  // ArraySeq(a, 1, b, 2, c, 3)

Mutable and Immutable Conversions

Converting between mutable and immutable collections requires explicit imports and understanding of the default collection types:

import scala.collection.mutable

val immutableList = List(1, 2, 3)
val mutableBuffer = immutableList.to(mutable.ListBuffer)
mutableBuffer += 4
println(mutableBuffer)  // ListBuffer(1, 2, 3, 4)

val backToImmutable = mutableBuffer.toList
println(backToImmutable)  // List(1, 2, 3, 4)

// Mutable and immutable sets
val immutableSet = Set(1, 2, 3)
val mutableSet = immutableSet.to(mutable.Set)
mutableSet += 4

// Mutable and immutable maps
val immutableMap = Map("a" -> 1, "b" -> 2)
val mutableMap = immutableMap.to(mutable.Map)
mutableMap("c") = 3

Converting mutable collections to immutable is a defensive copy operation:

import scala.collection.mutable

def processData(data: mutable.ArrayBuffer[Int]): List[Int] = {
  // Return immutable copy to prevent external modification
  data.toList
}

val buffer = mutable.ArrayBuffer(1, 2, 3)
val result = processData(buffer)
buffer += 4  // Doesn't affect result
println(result)  // List(1, 2, 3)

Specialized Conversions and Performance

Some conversions have specialized implementations for performance:

// Array to WrappedArray (zero-copy view)
val array = Array(1, 2, 3)
val wrapped: scala.collection.mutable.ArraySeq[Int] = array.toSeq.asInstanceOf[scala.collection.mutable.ArraySeq[Int]]

// Range to Vector (efficient)
val range = 0 until 1000000
val vector = range.toVector  // Efficient indexed structure

// Iterator conversions (one-time use)
val iterator = Iterator(1, 2, 3, 4, 5)
val list = iterator.toList
// iterator is now exhausted

Avoiding unnecessary conversions:

// Bad: multiple conversions
def processInefficient(data: Array[Int]): Array[Int] = {
  data.toList.filter(_ > 0).map(_ * 2).toArray
}

// Better: work with views or stay in one collection type
def processEfficient(data: Array[Int]): Array[Int] = {
  data.filter(_ > 0).map(_ * 2)
}

// Using views for lazy evaluation
def processLazy(data: List[Int]): List[Int] = {
  data.view.filter(_ > 0).map(_ * 2).toList
}

Handling Complex Conversion Scenarios

Real-world scenarios often require chaining conversions with transformations:

case class User(id: Int, name: String, age: Int)

val users = List(
  User(1, "Alice", 30),
  User(2, "Bob", 25),
  User(3, "Charlie", 35)
)

// Convert to map by ID
val userMap = users.map(u => u.id -> u).toMap

// Convert to map with grouped values
val usersByAge = users.groupBy(_.age)

// Convert nested structures
val nestedData = Map(
  "group1" -> List(1, 2, 3),
  "group2" -> List(4, 5, 6)
)

val flattenedArray = nestedData.values.flatten.toArray
println(flattenedArray.mkString(", "))  // 1, 2, 3, 4, 5, 6

Converting between Java and Scala collections:

import scala.jdk.CollectionConverters._

// Scala to Java
val scalaList = List(1, 2, 3)
val javaList = scalaList.asJava

// Java to Scala
val javaSet = new java.util.HashSet[String]()
javaSet.add("a")
javaSet.add("b")
val scalaSet = javaSet.asScala.toSet

// Mutable conversions
val javaMap = new java.util.HashMap[String, Int]()
javaMap.put("x", 1)
val scalaMap = javaMap.asScala.toMap

Understanding these conversion patterns enables writing efficient Scala code that balances immutability, performance, and interoperability requirements.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.