Skip to Content
Course content

39: Immutable vs Mutable Collections

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

Here is a snippet of code I reviewed recently from a junior dev. They were trying to build a simple system to track users currently logged into a chat room. It looks innocent enough, but it won't even compile.

fun main() {
    val activeUsers = listOf("Alice", "Bob", "Charlie")
    
    println("Adding a new user...")
    addUser(activeUsers, "Dave") 
    
    println(activeUsers)
}

fun addUser(users: List<String>, newUser: String) {
    users.add(newUser) // ❌ Compilation Error: Unresolved reference: add
}

The "ReadOnly" Wall

If you're coming from Java or Python, this feels wrong. You've created a list, you're passing it to a function, and you're telling that function to add an item. In those languages, lists are generally mutable by default. But in Kotlin, listOf() creates a List, and in Kotlin, the List interface is strictly read-only. It doesn't have an add(), remove(), or clear() method.

The developer here assumed that because the list was stored in a val, it was just the variable that was immutable, not the content. In reality, Kotlin separates the concept of the reference (val vs var) from the concept of the collection's capability (List vs MutableList).

Switching to MutableList for Dynamic Data

To fix this, we need to use mutableListOf(). This returns a MutableList, which extends List but adds all the modification methods we need. We also have to update the function signature so it explicitly accepts a MutableList.

fun main() {
    // We use mutableListOf here to allow changes
    val activeUsers = mutableListOf("Alice", "Bob", "Charlie")
    
    println("Adding a new user...")
    addUser(activeUsers, "Dave") 
    
    println(activeUsers) // Output: [Alice, Bob, Charlie, Dave]
}

// The parameter type must be MutableList to call .add()
fun addUser(users: MutableList<String>, newUser: String) {
    users.add(newUser)
}

Now the code compiles and works. But here is a professional tip: just because you can make everything mutable doesn't mean you should. If every function in your app can modify every list it touches, you'll spend your weekends chasing down bugs where data "mysteriously" disappeared or changed in a different part of the app.

Protecting Your State with Read-Only Views

I usually follow a pattern where I keep the mutation logic private and expose the data as a read-only List. This prevents external code from accidentally clearing my lists or adding garbage data.

Look at how I'd actually structure that user tracker in a real project:

class UserSessionManager {
    // Private mutable list: only this class can change it
    private val _users = mutableListOf<String>()

    // Public read-only view: others can see the users, but not change them
    val users: List<String> get() = _users

    fun addUser(user: String) {
        if (user.isNotBlank()) {
            _users.add(user)
        }
    }
}

By doing this, I'm using the type system to enforce my business rules. If some other developer tries to call manager.users.add("Hacker"), the compiler will stop them immediately. I've essentially created a "read-only window" into my private mutable data.




📋 Practical Task

Refactoring the Game Inventory System

You have been handed a PlayerInventory class that is causing bugs because any part of the game code can modify the player's items without going through the proper validation logic. Your task is to refactor this class to protect the internal state.

Requirements:

  • Change the internal items storage so it is private and mutable.
  • Expose the items to the rest of the game as a read-only List.
  • Ensure the addItem function only adds the item if the inventory has fewer than 5 items (the capacity limit).
// CURRENT BROKEN CODE
class PlayerInventory {
    val items = mutableListOf<String>()

    fun addItem(item: String) {
        items.add(item)
    }
}

fun main() {
    val inventory = PlayerInventory()
    inventory.addItem("Iron Sword")
    
    // BUG: External code can bypass the addItem logic and add 100 items!
    inventory.items.add("Cheat Item 1")
    inventory.items.add("Cheat Item 2")
    // ...
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.