Collection Factory Methods
Collection factory methods — List.of(...), Set.of(...), and Map.of(...)/Map.ofEntries(...) — introduced in Java 9, provide a concise, standard way to create small, fixed-content, genuinely immutable collections in a single expression, replacing the previously common but verbose combination of Arrays.asList(...), Collections.unmodifiableList(...), and manual add() calls, none of which produced a collection that was both concise to write and fully immutable. This entry covers exactly what problem these factories solve compared to the older idioms, the specific immutability and null-rejection guarantees they provide (which are stricter than Collections.unmodifiableXxx), their behavior around duplicate keys/elements, and important caveats around mutability expectations and performance for very small collections.
Motivation — The Old Idioms Were Verbose, Inconsistent, or Not Truly Immutable
// ── Arrays.asList — fixed-size, but inconsistently mutable ──────────────
List<String> al = Arrays.asList("a", "b", "c");
al.set(0, "z"); // OK — writes through to the backing array
System.out.println(al); // [z, b, c]
// al.add("d"); // UnsupportedOperationException — surprising inconsistency:
// set() works but add()/remove() don't, despite both seeming
// like "the list shouldn't change size or shouldn't change at all"
// ── Collections.unmodifiableList — wrapper, NOT truly immutable underneath
List<String> mutable = new ArrayList<>(List.of("x", "y"));
List<String> wrapped = Collections.unmodifiableList(mutable);
// wrapped.add("z"); // UnsupportedOperationException — wrapper blocks direct mutation
mutable.add("z"); // but the UNDERLYING list can still be mutated!
System.out.println(wrapped); // [x, y, z] — "unmodifiable" view just changed underneath us
// Old idiom required a multi-line construct-then-wrap process:
List<String> oldWay = Collections.unmodifiableList(new ArrayList<>(Arrays.asList("a", "b", "c")));
// ── Java 9+ — genuinely immutable, single expression ─────────────────────
List<String> newWay = List.of("a", "b", "c");
// newWay.add("d"); // UnsupportedOperationException
// There is no backing mutable collection anywhere — the immutability is structural,
// not just a wrapper around something that could still change elsewhereSet.of, Map.of, Map.ofEntries, and Null-Rejection
// ── Set.of — fail-fast on duplicates, unlike building a HashSet ────────
Set<String> s = Set.of("a", "b", "c");
// Set<String> bad = Set.of("a", "b", "a"); // IllegalArgumentException: duplicate element
// Contrast — old idiom silently deduplicates, hiding a likely mistake:
Set<String> hidden = new HashSet<>(Arrays.asList("a", "b", "a"));
System.out.println(hidden); // [a, b] — duplicate silently disappeared, no warning at all
// ── Map.of — alternating key/value, fine for small maps ────────────────
Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25, "Carol", 35);
// Map.of("k1", v1, "k1", v2); // IllegalArgumentException: duplicate key "k1"
// ── Map.ofEntries — scales better past a handful of pairs ──────────────
Map<String, Integer> manyAges = Map.ofEntries(
Map.entry("Alice", 30),
Map.entry("Bob", 25),
Map.entry("Carol", 35),
Map.entry("Dave", 40)
// ... can continue with no fixed upper limit, unlike Map.of's 10-pair overload ceiling
);
// ── No null elements/keys/values permitted anywhere ─────────────────────
// List.of("a", null, "c"); // NullPointerException — immediately, at creation
// Set.of("a", null); // NullPointerException
// Map.of("key", null); // NullPointerException — null VALUE rejected
// Map.of(null, "value"); // NullPointerException — null KEY rejected
// Contrast with mutable collections, which all happily permit null:
List<String> mutableList = new ArrayList<>();
mutableList.add(null); // perfectly legal — no exceptionCaveats — Iteration Order, Defensive Copies, and When Not to Use Them
// ── No guaranteed iteration order — and it's deliberately randomized ───
Set<String> set = Set.of("a", "b", "c", "d");
System.out.println(set); // order can differ between separate JVM runs of this exact code —
// NOT guaranteed to be insertion order or any other stable order
// Need predictable order? Use an explicitly ordered structure instead:
Set<String> ordered = new LinkedHashSet<>(List.of("a", "b", "c", "d")); // preserves insertion order
// ── Structural immutability does NOT freeze the elements themselves ────
class MutablePoint {
int x, y;
MutablePoint(int x, int y) { this.x = x; this.y = y; }
}
MutablePoint p = new MutablePoint(1, 2);
List<MutablePoint> points = List.of(p);
// points.add(new MutablePoint(3, 4)); // UnsupportedOperationException — list structure is frozen
p.x = 999; // perfectly legal — the ELEMENT itself is still fully mutable
System.out.println(points.get(0).x); // 999 — "immutable list", mutable contents
// ── Appropriate use case — small, fixed, known-in-advance values ───────
static final List<String> VALID_STATUSES = List.of("PENDING", "ACTIVE", "CLOSED");
static final Map<String, Integer> HTTP_STATUS_NAMES = Map.of(
"OK", 200, "NOT_FOUND", 404, "SERVER_ERROR", 500
);
// Good fit: fixed, small, known at compile time, never needs to grow
// Less appropriate — collection built incrementally via computation:
List<String> results = new ArrayList<>();
for (String item : someLargeDynamicSource) {
if (passesFilter(item)) results.add(transform(item));
}
// Keep this as a regular mutable ArrayList during construction;
// only wrap as immutable afterward if the caller truly needs that guarantee