CodeGym /Courses /JAVA 25 SELF /Mutable vs immutable collections: differences and use cas...

Mutable vs immutable collections: differences and use cases

JAVA 25 SELF
Level 34 , Lesson 3
Available

1. Introduction

Simply put, a mutable collection is one you can change after creation: add, remove, and modify elements. An immutable collection is a collection that cannot be changed after creation. Just like concrete after it sets: you can look at it and touch it, but you won’t be molding new shapes out of it.

A mutable collection is like a notebook with a pencil: you write, erase, add new notes. An immutable collection is like a laminated page: now no one can add or erase anything.

Examples of mutable collections

In Java, almost all standard collections are mutable by default. These include classes such as:

  • ArrayList
  • LinkedList
  • HashSet
  • TreeSet
  • HashMap
  • LinkedHashMap
  • and many others

Example: ArrayList

import java.util.*;

List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.set(1, "Charlie"); // replaced Bob with Charlie
names.remove("Alice");   // removed Alice
System.out.println(names); // [Charlie]

Here we can do anything with the collection: add, remove, swap elements. This is convenient when the collection is built dynamically, for example, while reading data from a file or user input.

2. Examples of immutable collections

These APIs, introduced in Java 9, may already be familiar, but they still take some getting used to:

  • List.of(...)
  • Set.of(...)
  • Map.of(...)
  • List.copyOf(collection)
  • Set.copyOf(collection)
  • Map.copyOf(map)

Example: List.of

List<String> planets = List.of("Mercury", "Venus", "Earth", "Mars");
System.out.println(planets); // [Mercury, Venus, Earth, Mars]
planets.add("Jupiter"); // Will throw UnsupportedOperationException!

An attempt to modify the collection results in a runtime exception.

Example: Collections.unmodifiableList

List<String> modifiable = new ArrayList<>(List.of("a", "b"));
List<String> unmodifiable = Collections.unmodifiableList(modifiable);
unmodifiable.add("c"); // UnsupportedOperationException!

But there’s a catch: if you modify the underlying collection, the wrapper changes as well!

modifiable.add("c");
System.out.println(unmodifiable); // [a, b, c] — the element appeared!

3. Key differences between mutable and immutable collections

Property Mutable Immutable
Can you add an element? Yes No
Can you remove an element? Yes No
Can you modify an element? Yes (e.g., set) No
Thread safety No (by default) Yes (no state—nothing to change)
Can you add null? Yes (usually) No (in Java 9+ factory methods)
Implementation ArrayList, HashSet, etc. List.of, Set.of, Map.of, copyOf

4. Why do we need immutable collections at all?

A natural question arises: if mutable collections are so flexible, why do we need immutable ones? There are many reasons, all related to safety, readability, and predictability of the code.

Safety and error prevention

When you expose a collection (for example, from a method or a class), you want to be sure no one will accidentally change its contents. This is especially important if the collection contains “important” data that must not change after initialization.

Example:

public class Team {
    private final List<String> players;

    public Team(List<String> players) {
        // Make an immutable copy so no one can replace the roster
        this.players = List.copyOf(players);
    }

    public List<String> getPlayers() {
        return players;
    }
}

Now any code that receives the list of players won’t be able to slip in its “friend.”

Thread safety

Mutable collections are unsafe when accessed from multiple threads simultaneously. Immutable collections, on the contrary, can be freely passed between threads—no one will be able to corrupt them.

Easier debugging

If a collection doesn’t change, you always know what it contains. You don’t have to fear that someone “quietly” modified it elsewhere in the code.

Using them as keys or values in other collections

Immutable objects are ideal candidates for use as keys in a Map or elements in a Set. If an object can change after being added, you risk losing access to it (see hashCode and equals).

5. When should you prefer mutable collections?

Mutable collections are great when:

  • The collection is built in stages, in a loop, or from different sources.
  • Frequent changes are required: adding, removing, sorting.
  • The collection is intended for internal use and no one “outside” can corrupt it.

Example: building a list

List<String> shoppingList = new ArrayList<>();
shoppingList.add("Milk");
shoppingList.add("Bread");
shoppingList.add("Apples");
// After building it, you can make an immutable version
List<String> finalList = List.copyOf(shoppingList);

6. When should you prefer immutable collections?

  • To store constant data (for example, the list of weekdays).
  • To pass collections across application layers (e.g., from DAO to service).
  • To return collections from methods to protect them from modification.
  • For multi-threaded scenarios where safety matters.

Example: constant data

public static final List<String> WEEKDAYS = List.of(
    "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"
);

Example: exposing outward

public List<String> getReadOnlyNames() {
    return List.copyOf(names); // no one will be able to modify the list
}

7. Nuances and pitfalls

Immutability ≠ thread safety

An immutable collection is protected from modifications, but that doesn’t mean it’s protected from other kinds of issues in a multithreaded environment (for example, if the collection’s elements are mutable objects).

List<List<String>> listOfLists = List.of(new ArrayList<>());
listOfLists.get(0).add("Oops!"); // You can change the inner list!

Wrappers vs copies

As discussed earlier, Collections.unmodifiableList is a wrapper, and if the underlying collection changes, the wrapper will change as well. Meanwhile, List.copyOf creates a true independent copy.

List<String> base = new ArrayList<>(List.of("a", "b"));
List<String> wrap = Collections.unmodifiableList(base);
List<String> copy = List.copyOf(base);

base.add("c");
System.out.println(wrap); // [a, b, c] — changed!
System.out.println(copy); // [a, b] — stayed the same!

NullPointerException

Factory methods (List.of, Set.of, Map.of) do not allow null:

List<String> bad = List.of("a", null); // Will throw NullPointerException at creation time!

8. Comparing approaches: pros and cons

Mutable collections

The main advantage of mutable collections is their flexibility. You can add and remove elements on the fly, rebuilding the collection as your program’s logic evolves. This approach is especially handy when you need to quickly assemble a temporary structure or change something dynamically.

But convenience comes at a price. A mutable collection always carries the risk of accidental interference: someone in the code might unintentionally change the data, leading to hard-to-trace bugs. In multithreaded programs it gets even worse—parallel modifications easily lead to races and unexpected failures. Managing the lifecycle of such data is also harder: you have to constantly keep in mind who can modify the collection and when.

Immutable collections

Working with immutable collections is calmer. You know for sure that no one will tweak them “behind your back.” This makes the code safer, easier to debug and test, and the collections themselves are easy to pass between threads without additional locking.

The downside is that you may have to sacrifice performance at times. If you need to add a new element, you have to create a new copy of the collection, which incurs extra memory and time costs. In addition, building large and complex structures directly in immutable form can be inconvenient. Usually they are built in a temporary mutable collection and, when everything is ready, “frozen.”

9. Common mistakes

Mistake No. 1: Returning a mutable collection to callers. If you return a plain ArrayList from a method, any external code can add or remove elements. This can lead to bugs that are very hard to track down.

Mistake No. 2: Using a wrapper instead of a copy. If you use Collections.unmodifiableList but the underlying collection is modified elsewhere, the “immutability” is an illusion.

Mistake No. 3: Mutable objects inside immutable collections. Even if the collection itself is immutable, its elements may be mutable. This can lead to unexpected state changes.

Mistake No. 4: Trying to add null to a collection created via factory methods. Unlike older collections, the new factory methods do not allow null—you’ll get a NullPointerException.

Mistake No. 5: Expecting a specific implementation. Collections created via List.of or Set.of do not guarantee a particular implementation type (it is not necessarily ArrayList or HashSet). Don’t rely on that.

1
Task
JAVA 25 SELF, level 34, lesson 3
Locked
Animal inventory: live list or static snapshot? 🐾
Animal inventory: live list or static snapshot? 🐾
1
Task
JAVA 25 SELF, level 34, lesson 3
Locked
Playlists: the set is immutable, but songs inside can change 🎶
Playlists: the set is immutable, but songs inside can change 🎶
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION