☕ Java

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

Before Java 9, creating a small, fixed collection inline required choosing among several imperfect idioms, each with a different drawback. Arrays.asList(...) is concise and widely used, but the List it returns is a fixed-size view backed directly by the array — set(...) is supported (it writes through to the backing array) but add(...) and remove(...) throw UnsupportedOperationException, an inconsistency that regularly surprised developers who assumed "fixed-size" meant "fully immutable" and were caught off guard when set() silently succeeded while add() failed. Collections.unmodifiableList(...) (and its Set/Map counterparts) produces a genuinely add/remove/set-blocking wrapper, but it is a view over an existing mutable collection — the wrapper itself blocks direct mutation, but the underlying collection can still be mutated through the original reference, meaning "unmodifiable" did not actually mean "immutable" in the deeper sense of the data never changing, and constructing one required first building the mutable collection and then separately wrapping it, a two-step, multi-line process for what was conceptually a single small fixed set of values. The Java 9 collection factory methods — List.of(e1, e2, ...), Set.of(e1, e2, ...), Map.of(k1, v1, k2, v2, ...) — solve this by producing collections that are genuinely, structurally immutable: there is no backing mutable collection anywhere that could be mutated to change the factory-created collection's contents, and every structural mutation method (add, remove, set, clear, put, etc.) throws UnsupportedOperationException unconditionally. The syntax is also dramatically more concise for the common case of "I just want this fixed handful of values as a collection," collapsing what previously took a multi-line construct-then-wrap idiom into a single inline expression.
Java
// ── 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 elsewhere

Set.of, Map.of, Map.ofEntries, and Null-Rejection

Set.of(...) creates an immutable Set with the same structural immutability guarantees as List.of(...), but with an additional constraint: it throws IllegalArgumentException immediately at creation time if any duplicate elements are passed, rather than silently deduplicating them the way constructing a HashSet from a list with duplicates would. This is a deliberate fail-fast design choice — passing duplicate elements to a set-factory call is almost always a programming mistake (the caller likely intended every element to be distinct), and silently discarding the duplicate would hide that mistake rather than surfacing it. Map.of(...) accepts an alternating sequence of key, value, key, value, ... arguments, which works cleanly for small maps but does not scale well past a handful of entries (the alternating-argument convention becomes error-prone and hard to read with many pairs, and there is a hard practical limit since Map.of overloads only go up to 10 key-value pairs). For larger fixed maps, Map.ofEntries(...) accepts a variable number of Map.Entry objects instead, each constructed via the static helper Map.entry(key, value), which reads more clearly when there are many entries and has no fixed upper limit on the number of pairs. Map.of, like Set.of, throws IllegalArgumentException immediately if duplicate keys are passed, again for fail-fast safety. A guarantee shared by all of these factory methods — List.of, Set.of, and Map.of/ofEntries — that differentiates them from many of the older mutable collection implementations (ArrayList, HashSet, HashMap all permit null elements/keys/values) is that none of them permit null anywhere: passing a null element, key, or value to any of these factory methods throws NullPointerException immediately at creation time, rather than allowing a null to be silently stored and potentially cause a confusing NullPointerException much later, far from where the null was actually introduced.
Java
// ── 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 exception

Caveats — Iteration Order, Defensive Copies, and When Not to Use Them

Set.of(...) and Map.of(...) (and Map.ofEntries) make no guarantee whatsoever about iteration order, and in practice the actual iteration order can vary between JVM runs of the very same program, deliberately randomized by the JDK's implementation specifically to prevent code from accidentally coming to depend on an iteration order that was never actually promised — this is a stricter and more aggressively-enforced version of the general principle that HashSet/HashMap iteration order shouldn't be relied upon either. Code that needs predictable iteration order over a small fixed collection should use a LinkedHashSet/LinkedHashMap constructed from the factory-created collection, or simply continue using an explicitly ordered structure, rather than relying on List.of (which does preserve insertion order, since it's a List) being mistaken for Set/Map's guarantees. A separate, easily-missed gotcha is that these factories do not perform a defensive copy when an array is passed via the varargs mechanism in certain contrived usages, and more importantly, that the resulting collection still holds direct references to whatever mutable objects were passed as elements — immutability here refers strictly to the collection's own structure (you cannot add, remove, or replace elements), not to the elements' own internal mutability. A List.of(someMutableObject) prevents replacing someMutableObject within the list, but does not prevent calling a mutator method on someMutableObject itself, which is a distinction worth being explicit about since "immutable list" can be misread as "everything in this list is now frozen." Finally, these factories are best suited for genuinely small, fixed collections of known values — configuration constants, a small fixed enumeration of allowed values, test fixtures — rather than as a general-purpose replacement for building up a list through computation; for collections that need to grow incrementally or be mutated during construction, the ordinary mutable collection classes followed by an explicit wrap (or simply staying mutable, if mutation continues to be needed) remain the appropriate choice.
Java
// ── 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

Related Topics in Java 9 to 21 Features

Module System (JPMS)
The Java Platform Module System (JPMS), introduced in Java 9 under Project Jigsaw, adds a module as a new, higher-level unit of code organization above the package, allowing a JAR-like artifact to explicitly declare which packages it exports for use by other modules, which modules it depends on, and which packages it keeps entirely internal and inaccessible from outside — closing a long-standing gap in the classpath model where every public class in every JAR on the classpath was accessible to every other JAR, regardless of whether that access was intended. JPMS was also the mechanism used to decompose the JDK itself, which had grown into one monolithic rt.jar, into roughly 90 separate modules, enabling smaller custom runtime images and explicit internal-API encapsulation (notably restricting access to sun.* and other internal packages that many libraries had relied on despite warnings). This entry covers the classpath problems JPMS solves, module-info.java syntax and the requires/exports/opens directives, the distinction between the classpath and module path including automatic and unnamed modules, and jlink custom runtime images.
JShell
JShell, introduced in Java 9, is an interactive Read-Eval-Print Loop (REPL) bundled directly with the JDK, allowing Java statements, expressions, method, class, and variable declarations to be entered and evaluated one at a time at an interactive prompt, with immediate feedback — without requiring a class wrapper, a main method, explicit compilation, or a build tool, all of which Java had always required for even the smallest snippet of executable code before Java 9. This solved a long-standing friction point for learning, quick experimentation, and API exploration, where languages like Python or Ruby offered an immediate interactive prompt but Java required the full edit-compile-run cycle even to test a single line. This entry covers what problem JShell solves and how it differs from a script, core REPL usage including implicit variable creation and forward references, snippet management commands, and how to feed JShell external classes and files.
Private Interface Methods
Private interface methods, introduced in Java 9, allow an interface to declare methods marked private — with a full method body — that are accessible only from within that same interface (from its default methods, its other private methods, and its private static methods), but completely invisible to implementing classes and to any code outside the interface. They close a code-duplication gap left after Java 8's default methods: when an interface has multiple default methods that share common logic, that shared logic previously either had to be duplicated in each default method or exposed as a public default method purely to enable reuse, polluting the interface's actual API surface with a method meant only for internal reuse. This entry covers the specific problem they solve, the distinction between private instance and private static interface methods, the rules governing what they can call and be called from, and how they interact with the broader default-method design Java established in version 8.
var Keyword
The var keyword, introduced in Java 10 as local variable type inference, lets a local variable declaration omit its explicit type and have the compiler infer it from the initializer expression on the right-hand side, while the variable remains just as statically typed at compile time as if the type had been written out explicitly — var is purely a source-code convenience, not a shift toward dynamic typing. This is a narrower, more conservative feature than type inference in languages like C# (var) or Kotlin (val/var): it applies only to local variables with an initializer, never to fields, method parameters, or return types, and the compiler still performs full static type checking using the inferred type. This entry covers the motivation and explicit scope restrictions, how type inference actually resolves (including with generics, diamond operator interaction, and anonymous classes), the readability tradeoffs and style guidance the OpenJDK team itself published, and the specific cases where var cannot be used.