Immutable List in Java
Problem
List.of() (Java 9+) builds a List that can never be modified after creation — not a mutable list with restrictions bolted on, but a dedicated immutable implementation.
Create a List of elements that can never be added to, removed from, or changed after it's built.
Java Program
import java.util.List;
public class ImmutableListDemo {
public static void main(String[] args) {
List<String> fruits = List.of("Apple", "Banana", "Cherry");
System.out.println(fruits); // fruits.add(...) here would throw UnsupportedOperationException
}
}Output
Core Logic
Building the list through the List.of() factory method, instead of a constructor, returns an object whose class doesn't implement mutation at all.
- 1
List.of("Apple", "Banana", "Cherry")returns a List already fully populated with those three elements. - 2The returned object isn't an
ArrayList— it's a distinct, package-private implementation whose add/remove/set methods are all deliberately unsupported. - 3Reading from it works exactly like any other List —
get(), iteration, and printing all behave normally. - 4Calling any mutating method on it, like
.add("Date"), throwsUnsupportedOperationExceptionimmediately, rather than silently succeeding or failing later.
List.of("Apple", "Banana", "Cherry") prints as [Apple, Banana, Cherry], and reading its elements works normally.Key Point: This is a genuinely different guarantee from wrapping an existing mutable list — there's no underlying mutable list anywhere for other code to change out from under you, since List.of() never creates one in the first place.
Why: List.of() stores each of the n given elements once, in a fixed internal structure sized for exactly that many.
Key Concepts
Approach 2: Java 8
import java.util.List;
import java.util.stream.Collectors;
public class ImmutableListStream {
public static void main(String[] args) {
List<String> source = List.of("Kiwi", "Banana", "Fig", "Cherry");
// Filters first, then collects the result into a genuinely immutable list
List<String> longNames = source.stream()
.filter(s -> s.length() > 5)
.collect(Collectors.toUnmodifiableList());
System.out.println(longNames); // longNames.add(...) here would throw UnsupportedOperationException
}
}
Output
Core Logic
Collectors.toUnmodifiableList() builds an immutable list from a stream pipeline directly, for the common case where the elements come from filtering or transforming existing data rather than being known ahead of time as literals.
- 1
source.stream()opens a stream over an existing mutable source list. - 2
.filter(s -> s.length() > 5)narrows the stream down to only the elements meeting some condition — somethingList.of()can't express, since it just takes a fixed set of elements directly. - 3
.collect(Collectors.toUnmodifiableList())gathers the filtered results into a genuinely immutable list, the sameUnsupportedOperationException-on-mutation guaranteeList.of()provides. - 4Calling
.add(...)on the result throws exactly as it would on aList.of()result.
[Kiwi, Banana, Fig, Cherry] down to names longer than 5 characters and collecting produces the immutable list [Banana, Cherry].Key Point: List.of() is for elements already known as literals; Collectors.toUnmodifiableList() is for the more common real-world case — an immutable list built from data that was filtered, mapped, or otherwise derived first.
Why: filter() visits each of the n source elements once, and collect() builds a new immutable list to hold the surviving elements.