Skip to Content
Course content

50: Immutable Collections

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

You've probably spent a lot of time now working with ArrayList and HashSet. They're great, but they're "leaky." When you pass a mutable list to another class or return one from a getter, you're essentially handing over the keys to your internal state. Anyone with that reference can add or remove elements, and suddenly your application is in a state you never intended.

In a professional codebase, we want to avoid this. We want immutable collections—lists, sets, or maps that cannot be changed after they are created. Let's build a small configuration system for a game level to see this in action.

Defining a Level Configuration

Imagine we're building a game where each level has a specific set of "allowed items." Once the level starts, those items shouldn't change. If a rogue piece of code adds a "God Mode Sword" to a tutorial level, we've got a problem.

public class LevelConfig {
    private final String levelName;
    private final List<String> allowedItems;

    public LevelConfig(String levelName, List<String> items) {
        this.levelName = levelName;
        // I'll start by just assigning the list directly
        this.allowedItems = items; 
    }

    public List<String> getAllowedItems() {
        return allowedItems;
    }

    public String getLevelName() {
        return levelName;
    }
}

Now, if I'm the one writing the LevelConfig class, I might feel safe. But look what happens when I use it:

List<String> items = new ArrayList<>();
items.add("Wooden Shield");
items.add("Rusty Sword");

LevelConfig tutorial = new LevelConfig("Tutorial", items);
tutorial.getAllowedItems().add("Nuclear Bomb"); // Wait, what?

Because I returned the reference to the original ArrayList, the caller can modify my internal state. That's a disaster.

The "Unmodifiable" Trap

My first instinct years ago was to use Collections.unmodifiableList(). It sounds like exactly what we need, right? It returns a wrapper that throws an exception if you try to call .add() or .remove(). Let me try that in our constructor.

public LevelConfig(String levelName, List<String> items) {
    this.levelName = levelName;
    this.allowedItems = Collections.unmodifiableList(items);
}

This looks better. If the caller tries tutorial.getAllowedItems().add("Nuclear Bomb"), Java will throw an UnsupportedOperationException. I feel smug for a second. But then I realize I made a classic mistake. I'm still holding a reference to the original items list passed into the constructor.

List<String> myItems = new ArrayList<>();
myItems.add("Wooden Shield");

LevelConfig tutorial = new LevelConfig("Tutorial", myItems);
myItems.add("Nuclear Bomb"); // I'm modifying the source list!

System.out.println(tutorial.getAllowedItems()); // The bomb is in there!

The unmodifiableList is just a view. If the underlying list changes, the view changes too. This is why "unmodifiable" is not the same thing as "immutable."

Switching to True Immutability

To actually fix this, we need to disconnect the internal list from the outside world entirely. Since Java 10, we have List.copyOf(). This doesn't just wrap the list; it creates a brand new, truly immutable copy. If the original list changes later, our LevelConfig remains untouched.

Here is the corrected, professional version of our class:

public class LevelConfig {
    private final String levelName;
    private final List<String> allowedItems;

    public LevelConfig(String levelName, List<String> items) {
        this.levelName = levelName;
        // This creates a truly immutable copy
        this.allowedItems = List.copyOf(items);
    }

    public List<String> getAllowedItems() {
        // Since allowedItems is already immutable, we can return it safely
        return allowedItems;
    }

    public String getLevelName() {
        return levelName;
    }
}

Now, whether the caller tries to modify the list through the getter or modifies the list they passed into the constructor, the LevelConfig stays exactly as it was intended. We can do the same thing with Set.copyOf() and Map.copyOf(). If you're creating a list from scratch and know it'll never change, you can also use List.of("Item1", "Item2") to create an immutable list immediately.




📋 Practical Task

Exercise: Hardening a User Permission Registry

You are tasked with fixing a security flaw in a PermissionRegistry class. Currently, the class allows external code to bypass security by modifying the permission sets after they have been registered.

Your Goal: Modify the PermissionRegistry class so that the permissions for any given role cannot be changed once they are added to the registry, regardless of whether the caller has a reference to the original list.

import java.util.*;

public class PermissionRegistry {
    private final Map<String, List<String>> rolePermissions = new HashMap<>();

    public void registerRole(String role, List<String> permissions) {
        // BUG: This is storing a mutable reference
        rolePermissions.put(role, permissions);
    }

    public List<String> getPermissionsForRole(String role) {
        // BUG: This returns a mutable reference
        return rolePermissions.get(role);
    }

    public static void main(String[] args) {
        PermissionRegistry registry = new PermissionRegistry();
        List<String> adminPerms = new ArrayList<>(Arrays.asList("READ", "WRITE", "DELETE"));
        
        registry.registerRole("ADMIN", adminPerms);
        
        // Scenario 1: Modifying the original list
        adminPerms.add("SUPERUSER"); 
        
        // Scenario 2: Modifying via the getter
        registry.getPermissionsForRole("ADMIN").add("ROOT_ACCESS");

        System.out.println("Admin Permissions: " + registry.getPermissionsForRole("ADMIN"));
        // Expected output for a secure system: [READ, WRITE, DELETE]
        // Current output: [READ, WRITE, DELETE, SUPERUSER, ROOT_ACCESS]
    }
}

Apply the List.copyOf() and Map.copyOf() (or Collections.unmodifiableMap()) concepts to ensure the PermissionRegistry is truly immutable once data is entered.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.