Back to Blog
Java

Java Immutable List: Creation and Practical Use

java immutable list: Learn how to create immutable lists in Java using List.of, Collections.unmodifiableList, and streams. Understand null handling, performance, and t...

immutable-listjava-collectionslist-ofunmodifiable-listjava-17
A Java code snippet showing List.of creating an immutable list, with a lock icon representing immutability.

In Java, a list created with new ArrayList<>() is mutable by default. For many applications, that mutability is a source of bugs, especially when a list is passed across method boundaries. A Java immutable list cannot be modified after creation, which makes it safe to share without defensive copies. This article covers the common ways to create one, the behavioral differences between them, and the tradeoffs you need to consider.

The Core Problem: Mutability in Java Lists

Most Java developers reach for ArrayList or LinkedList when they need a list. These classes implement the List interface with full mutability: you can add, remove, and replace elements at any time. That flexibility is useful, but it also means that any code holding a reference to the list can change its contents. A method that receives a mutable list as a parameter cannot assume the list will stay the same while it works with it.

Consider a configuration object that exposes a list of allowed roles. If the list is mutable, a caller can add a role after validation has already run, potentially bypassing security checks. Even without malicious intent, accidental modification is common when the same list is shared between different parts of an application. An immutable list eliminates this entire class of problems because the list itself rejects any structural modification.

List.of: The Modern Way to Create an Immutable List

Java 9 introduced List.of, a static factory method that returns an immutable list. It is the simplest way to create a Java immutable list when you know the elements at creation time.

List<String> roles = List.of("admin", "editor", "viewer");

The returned list is truly immutable. Calling add, remove, or set throws UnsupportedOperationException. The list is also fixed in size, so there is no way to change its length. This is a deliberate design choice that allows the JVM to optimize storage. For small lists, the implementation can use a compact representation that avoids the overhead of an ArrayList's internal array and modCount tracking.

One important detail is that List.of does not accept null elements. If you pass a null to List.of, it throws NullPointerException immediately. This is a useful guarantee because it means the list can never contain a null value, which simplifies downstream code that iterates the list.

Collections.unmodifiableList: Wrapping an Existing List

Before Java 9, the standard approach was Collections.unmodifiableList. This method returns a read-only view of an existing list. The view prevents modifications through the returned reference, but the original list remains mutable.

List<String> original = new ArrayList<>(); original.add("one"); List<String> unmodifiable = Collections.unmodifiableList(original);

If you later modify original, the change is visible through unmodifiable. This behavior is often surprising. The wrapper only blocks calls that go through the unmodifiable reference; it does not copy the underlying data. If you need a truly independent immutable list, you must copy the original list before wrapping it.

List<String> copied = new ArrayList<>(original); List<String> immutable = Collections.unmodifiableList(copied);

Even then, the list is only immutable as long as no one holds a reference to copied. This is why List.of is generally preferred for new code: it returns a list with no backing mutable structure.

Arrays.asList and Other Pitfalls

Arrays.asList is another common source of confusion. It returns a fixed-size list backed by the specified array. You cannot add or remove elements, but you can call set to replace an existing element. That means it is not immutable.

String[] array = {"a", "b", "c"}; List<String> fixed = Arrays.asList(array); fixed.set(0, "z"); // allowed fixed.add("d"); // throws UnsupportedOperationException

Because the list is backed by the array, changes to the array also affect the list. This is rarely what you want when you need an immutable list. If you accidentally use Arrays.asList and then pass the result to code that expects immutability, you may introduce subtle bugs.

Another pitfall is using Collections.unmodifiableList on a list that is later modified. Even if you wrap an ArrayList, the wrapper is only as immutable as the underlying list. If the original list is changed, the wrapper reflects those changes. This is not a defect in the API; it is the documented behavior. The key is to understand which layer provides the immutability guarantee.

Null Elements and Duplicate Behavior

List.of rejects null elements, but it allows duplicates. For example, List.of("a", "a") is valid and returns a list with two elements. This is different from Set.of, which also rejects null but does not allow duplicates. The duplicate behavior is consistent with the List contract, which does not require uniqueness.

If you need a list that can contain null, List.of is not suitable. You can use Collections.unmodifiableList with a list that allows null, but you lose the static guarantee. In practice, most developers prefer to avoid null in collections altogether. If you are using a newer Java version, you can use Objects.requireNonNull before adding elements to enforce your own rule.

When you create an immutable list from a stream, you can use Collectors.toUnmodifiableList(), which was added in Java 10. This collector produces a list that is not modifiable, and it also rejects null elements. It behaves similarly to List.of in that respect.

List<String> result = stream .filter(Objects::nonNull) .collect(Collectors.toUnmodifiableList());

This is convenient when you already have a stream and want to collect into an immutable structure without copying it again.

Performance and Memory Characteristics

Immutable lists from List.of are typically more memory-efficient than a wrapped ArrayList. The JVM can use a specialized implementation that stores the elements in a compact array without the extra capacity that ArrayList reserves for future growth. There is also no wrapper object, so the overhead is lower.

For Collections.unmodifiableList, the wrapper adds one object, and the underlying list still has its own internal array. If the original list is an ArrayList with extra capacity, that memory is wasted because the list can never grow. Copying the list to a new ArrayList with new ArrayList<>(original) copies the elements but may still allocate more capacity than needed. In contrast, List.of creates a list with exactly the number of elements you provide.

Performance is rarely the deciding factor for small lists, but it can matter when you create many immutable lists in a hot path. For example, a configuration loader that builds a list of allowed values for every request should avoid creating a new mutable list and then wrapping it. Using List.of directly is both simpler and more efficient.

There is one runtime cost to consider with List.of: the implementation may use a varargs array for the elements. If you call List.of with many elements, that array is allocated. For most use cases this is negligible, but if you are creating millions of lists with large element counts, you may want to measure the impact.

Choosing the Right Approach for Your Code

Use List.of when you know the elements at compile time and you want a truly immutable list with no backing mutable structure. It is the clearest expression of intent and provides the strongest guarantees.

Use Collections.unmodifiableList when you need to create an immutable view of an existing list that you do not own, or when you need to allow null elements. Be aware that the view reflects changes to the original list. If that is not acceptable, copy the list first.

Use Arrays.asList only when you need a fixed-size list backed by an array and you intend to modify elements through set. It is not an immutable list, so do not pass it to code that expects immutability.

For stream pipelines, Collectors.toUnmodifiableList() is the idiomatic choice. It produces a list that is not modifiable and rejects null, matching the behavior of List.of. This avoids the extra copy that would be needed if you collected into a mutable list and then wrapped it.

When you are designing an API, prefer returning an immutable list. This tells callers that they cannot modify the result, which makes the contract explicit. It also prevents accidental modification from leaking into internal state. For example, a method that returns a list of default permissions should return List.of(...) rather than a mutable ArrayList. This small change reduces the risk of future bugs without adding any complexity.

java immutable list: Practical Usage and Code Examples | RYUSLOG DEV