Skip to Content
Course content

58: Mutable vs Immutable Collections In Depth

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're managing a project on a physical whiteboard in an office. Anyone can walk up to it, erase a task, and write in a new one. If you look at the board on Monday, it says one thing; by Tuesday, it's completely different. That whiteboard is mutable. The problem? If you need to know what the board looked like on Monday to figure out where a mistake happened, you're out of luck. It's gone.

Now, imagine instead that every time a change is made, you take a high-resolution Polaroid photo of the board and hand it to the team. The original photo never changes. If you want to add a task, you take a new photo that includes the old tasks plus the new one. This is immutable. You have a perfect, unchangeable history, and you can hand a photo to a teammate without worrying that they'll scribble over it while you're still looking at it.

Mapping the Polaroid to the Code

In Scala, this isn't just a philosophical choice; it's baked into the library hierarchy. Most of the time, you'll be using scala.collection.immutable. When you "add" an element to an immutable List, you aren't actually changing the list—you're creating a new one that shares most of its structure with the old one.

val initialInventory = List("Sword", "Shield")
val updatedInventory = "Potion" :: initialInventory

println(initialInventory) // Still List(Sword, Shield)
println(updatedInventory) // List(Potion, Sword, Shield)

In the example above, initialInventory is like that first Polaroid. It's locked. updatedInventory is a new photo. I love this because it eliminates a whole category of bugs where one part of your app accidentally changes a list that another part of your app was relying on.

The Cost of the "New Photo"

You might be thinking: "Wait, if I have 10,000 items, am I really copying the whole list every time I add one item? That sounds like a performance nightmare."

This is where Scala's "structural sharing" comes in. For a List, adding an element to the front (prepending) doesn't copy the rest of the list; the new list simply points to the old list as its "tail." It's incredibly efficient. However, if you need fast random access (like grabbing the 500th element), a List is slow because it has to walk from the start. That's why we have Vector. It's also immutable, but it uses a tree structure under the hood to make updates and lookups much faster.

When to Embrace the Whiteboard

I'll be honest with you: being a purist about immutability can sometimes lead to awkward code or poor performance in very specific scenarios. If you're in a tight loop updating a value 100,000 times a second, creating 100,000 immutable objects will put a lot of pressure on the Garbage Collector.

This is when you reach for scala.collection.mutable. An ArrayBuffer, for instance, is essentially that whiteboard. You modify it in place.

import scala.collection.mutable.ArrayBuffer

val scores = ArrayBuffer(10, 20, 30)
scores += 40 // This modifies the existing object in memory
println(scores) // ArrayBuffer(10, 20, 30, 40)

My rule of thumb? Start immutable. Always. If you find a specific bottleneck in your profiling, or if you're implementing a complex local algorithm where a mutable buffer makes the logic significantly cleaner, switch to a mutable collection. But keep that mutability local. Don't let a mutable collection leak out of a class or a function; return it as an immutable List or Vector so the rest of your system stays safe.




📋 Practical Task

Build a Versioned Game Inventory System

You need to create a system that tracks a player's inventory but allows them to "Undo" an item pickup. Since we want to preserve history, you cannot use a mutable collection for the history itself.

Requirements:

  • Create a case class GameState that holds an immutable List[String] of items.
  • Implement a GameManager class that maintains a List[GameState] representing the history of the game.
  • Add a method pickupItem(item: String): This should create a new GameState with the item added and push this new state onto the history list.
  • Add a method undoPickup(): This should remove the most recent state from the history, effectively reverting the inventory to the previous version.
  • Add a method currentInventory: This should return the items in the most recent state.

Goal: Demonstrate that by using a list of immutable states, you can travel back in time without ever manually "removing" an item from a mutable list.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.