1. Introduction
Scala has a very rich collections library, located under the scala.collection package.
In this tutorial, we’ll do a quick overview of the common Scala collections and how to use them.
2. Types of Scala Collections
There are two types of collections in Scala – mutable and immutable. Let’s point out some differences.
2.1. Mutable Collections
A mutable collection updates or extends in place. This means we can add, change or remove elements of a collection as a side effect. These collections have operations that change the collection in place. All mutable collection classes are present in the scala.collection.mutable package.
2.2. Immutable Collections
All immutable collections are present under scala.collection.immutable. We can perform operations such as additions, removals, or updates, but these operations always return a new collection and leave the old collection unchanged.
3. Scala Collection Hierarchy Overview
Scala’s collection classes begin with the Traversable and Iterable traits. These traits branch into three main categories: List, Set, and Map.
The Traversable trait allows us to traverse an entire collection. It’s a base trait for all other collections. It implements the common behavior in terms of a foreach method.
The Iterable trait is the next trait from the top of the hierarchy and a base trait for iterable collections. It defines an iterator which allows us to loop through a collection’s elements one at a time. When we use an iterator, we can traverse the collection only once. This is mainly because each element gets processed during the iteration process.
Now, let’s explore some of the most common immutable collections of the Scala library.
4. Most Commonly Used Scala Collections
4.1. List
Scala lists internally represent an immutable linked list. It maintains the order of elements and can contain duplicates as well. This class is optimal for last-in-first-out (LIFO), stack-like access patterns.
It also implements structural sharing of the tail list. This means that many operations have either a constant memory footprint or no memory footprint at all.
A list has O(1) prepend and head/tail access. Most other operations are O(n) though; this includes length, append, reverse, and also the index-based lookup of elements.
We can, for example, declare a list of integers:
val numbersList: List[Int] = List(1, 2, 3 ,4)
val emptyList: List[Int] = List() // Empty List
This List class comes with two implementing case classes, scala.Nil and scala.::, that implement the abstract members isEmpty, head, and tail. A Scala list containing elements can be represented using x :: xs, where x is the head and the xs is the remaining list. Nil represents an empty list:
val numbersList: List[Int] = 1 :: 2 :: 3 :: 4 :: Nil // List of Integers
val emptyList: List[Int] = Nil // Empty List
val x :: xs = numbersList
assert(x == 1) // true
assert(xs == List(2, 3, 4)) // true
In the above example, we can represent the numbersList using the x :: xs notation. While printing the value of x and xs, we find that x is the head of the list, and xs is the remaining list.
There are three basic operations on lists:
No.
Method
Description
1
head
Returns the first element of the List
2
tail
Returns a List consisting of all elements except head (the first element)
3
isEmpty
Returns true if the List is empty
Let’s try using each one in turn:
val numbersList: List[Int] = 1 :: 2 :: 3 :: 4 :: Nil
assert(numbersList.head == 1) // true
assert(numbersList.tail == List(2, 3, 4)) // true
assert(numbersList.isEmpty) // false
Other common operations include concatenating two lists, creating uniform lists, and reversing a list:
List(1,2) ::: List(3,4) // List(1, 2, 3, 4)
List.fill(3)(100) // List(100, 100, 100)
List(1,2,3,4).reverse // List(4, 3, 2, 1)
We can find the complete list of Scala List methods in the ScalaDoc.
4.2. Set
Scala Set is a collection of unique elements. By default, Scala uses an immutable set. It doesn’t maintain any order for storing elements.
We can declare an immutable set as:
val emptySet: Set[Int] = Set() // Empty set
val numbersSet: Set[Int] = Set(1, 2, 3, 4) // Set of integers
If we want to use a mutable Set, we need to import it from the collection.mutable explicitly:
val mutableSet = collection.mutable.Set(1, 2, 3)
The operations on a Set are similar to the ones on the List:
No.
Method
Description
1
head
Returns the first element of a Set
2
tail
Returns a Set consisting of all elements except head (the first element)
3
isEmpty
Returns true if the Set is empty
So, let’s try them out:
Set(1, 2, 3, 4).head // 1
Set(1, 2, 3, 4).tail // Set(2, 3, 4)
Set(1, 2, 3, 4).isEmpty // false
The complete list of methods of Scala Set is in the ScalaDoc.
4.3. Map
A Map is a collection of key/value pairs where keys are always unique. Scala provides mutable and immutable versions of it. By default, an immutable version of the map is imported:
val immutableMap = Map(1 -> "a", 2 -> "b")
val mutableMap = collection.mutable.Map(1 -> "a", 2 -> "b")
The methods for working with maps are bit different:
No.
Method
Description
1
keys
Returns an iterable containing all keys of the Map
2
values
Returns an iterable containing all values of the Map
3
isEmpty
Returns true if the Map is empty
Let’s see how these methods work:
Map(1 -> "a", 2 -> "b").keys // res0: Iterable[Int] = Set(1, 2)
Map(1 -> "a", 2 -> "b").values // res1: Iterable[String] = Iterable(a, b)
Map(1 -> "a", 2 -> "b").isEmpty // false
The get method returns an optional value. Its signature in the Map trait is as follows:
def get(key: K): Option[V]
When the key exists, it returns the value in Some context, whereas if the key does not exist, it returns None:
Map(1 -> "a", 2 -> "b").get(1) // Some(a)
Map(1 -> "a", 2 -> "b").get(3) // None
We can find the complete list of methods of Scala Map in the ScalaDoc.
4.4. Tuple
A Tuple is a collection that gives us a way to store different items in the same container. It combines a fixed number of items. We can pass this as a whole, and there’s no need to declare a class separately.
Scala 2.x has classes named Tuple2, Tuple3 … up to Tuple22.
If we just place some elements inside parentheses, we get a Tuple. A tuple of int and String would look like:
val t1 = (1, "A")
The declaration t1 is just syntactic sugar for a Tuple:
val t1 = Tuple2(1, "A")
There are two ways to access a tuple’s elements. The first way is to access them by the element number:
val tuple3 = (1, "One", "A") // tuple3: (Int, String, String)
tuple3._1 // 1
tuple3._2 // One
tuple3._3 // A
The second way of accessing elements is by using the classical pattern matching in Scala. In this way, we can assign the Tuple elements to some appropriate variable names:
val (num, word, char) = (1, "One", 'A')
num // 1
word // One
char // A
We can iterate over a Tuple using the productIterator method:
val tuple = (1,2,3,4)
tuple.productIterator.foreach(println)
which would, in this case, output:
1
2
3
4
Technically, Scala 2.x tuples are not collections classes and hence they do not extend the Iterable trait.
5. ArrayBuffer
The ArrayBuffer collection is part of the mutable collection framework provided by the Scala standard library. It provides dynamic resizing capabilities, efficient element addition/removal, and random access by index.
Unfortunately, compared to other collections, such as List, Map, and Set, it’s slightly less known among beginners. So, let’s go ahead and devote some time to learn about this interesting collection in more detail.
First, let’s start by creating an empty ArrayBuffer:
val buffer = ArrayBuffer.empty[Int]
buffer should be(empty)
Next, let’s see how we can use the += operator to add elements to the buffer ArrayBuffer:
buffer += 1
buffer += 2
buffer += 3
Further, let’s go ahead and verify the in-place mutation operation that we just did:
buffer should have size 3
buffer(0) shouldEqual 1
buffer(1) shouldEqual 2
buffer(2) shouldEqual 3
Perfect! The results are as expected. Additionally, we must note that we can access the elements of the buffer using a zero-based index, similar to an Array.
Now, let’s see how we can efficiently access and update an element in the buffer ArrayBuffer using a valid index:
buffer(1) = 5
buffer shouldEqual ArrayBuffer(1, 5, 3)
As expected, we can observe that the element at the first index has changed.
Moving on, let’s also learn about the use of the -= operator for removing an element from buffer:
val buffer = ArrayBuffer(1, 2, 3)
buffer -= 2
buffer shouldEqual ArrayBuffer(1, 3)
Great! We’ve successfully validated that the desired element is no longer present in buffer.
Lastly, let’s also see a scenario where we try to access an element in buffer using an invalid index:
val buffer = ArrayBuffer(1, 2, 3)
assertThrows[IndexOutOfBoundsException] {
buffer(5)
}
We can note that the invalid access to the fifth position threw IndexOutOfBoundsException because buffer contains only three elements.
6. Conclusion
In this article, we explored Scala’s collection library. We looked at the differences between mutable and immutable collections and explored the commonly used collections in Scala. Furthermore, we learned about the ArrayBuffer collection and its interesting properties.
The complete code is available over on GitHub.