Collection Factory Methods (JEP 269): Immutable List, Set, and Map
Why Collection Factory Methods?
Before Java 9, creating a small immutable collection was tedious:
// Java 8 — three lines, two classes, mutable intermediate
List<String> names = Collections.unmodifiableList(
Arrays.asList("Alice", "Bob", "Charlie")
);
// Java 8 — even worse for Map
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 85);
Map<String, Integer> immutableScores = Collections.unmodifiableMap(scores);
JEP 269 (Java 9) introduced static factory methods that create truly immutable collections with a single expression:
var names = List.of("Alice", "Bob", "Charlie");
var scores = Map.of("Alice", 90, "Bob", 85);
List.of()
List<String> empty = List.of();
List<String> one = List.of("Alice");
List<String> two = List.of("Alice", "Bob");
List<String> many = List.of("Alice", "Bob", "Charlie", "Dave");
// Iteration
for (var name : List.of("Alice", "Bob")) {
System.out.println(name);
}
// Usage as method argument
process(List.of(1, 2, 3, 4, 5));
Guarantees
- Immutable:
add(),remove(),set(), andclear()throwUnsupportedOperationException. - Null-free: any
nullelement throwsNullPointerExceptionimmediately. - Serialisable: all implementations are
Serializable. - Order preserved: iteration order matches the factory argument order.
- No identity guarantee:
List.of("a") == List.of("a")may befalse— do not rely on reference equality.
var list = List.of("a", "b");
list.add("c"); // UnsupportedOperationException
list.set(0, "x"); // UnsupportedOperationException
list.get(0); // "a" — reads are fine
List.of("a", null); // NullPointerException — null not allowed
Performance
List.of() uses compact internal representations:
- 0 elements: singleton
List12orListNinstance (shared) - 1–2 elements: specialised
List12class (no array allocation) - 3+ elements:
ListNbacked by a single array
This is more memory-efficient than Arrays.asList() (which wraps the original array with a full AbstractList wrapper) or ArrayList (which allocates an array with initial capacity 10).
Set.of()
Set<String> roles = Set.of("ADMIN", "USER", "MODERATOR");
Set<Integer> primes = Set.of(2, 3, 5, 7, 11);
roles.contains("ADMIN") // true
roles.size() // 3
Guarantees
- Immutable: mutation throws
UnsupportedOperationException. - Null-free:
NullPointerExceptionif any element isnull. - No duplicates: duplicate elements throw
IllegalArgumentExceptionat creation time. - Iteration order not guaranteed: unlike
List.of(), set iteration order is undefined and may vary between JVM runs.
Set.of("a", "a"); // IllegalArgumentException — duplicate element
Set.of("a", null); // NullPointerException — null not allowed
Map.of()
Up to 10 key-value pairs, specified as alternating key and value arguments:
Map<String, Integer> httpCodes = Map.of(
"OK", 200,
"Created", 201,
"BadRequest", 400,
"NotFound", 404,
"ServerError", 500
);
httpCodes.get("OK") // 200
httpCodes.size() // 5
Guarantees
- Immutable:
put(),remove(),putAll()throwUnsupportedOperationException. - Null-free:
NullPointerExceptionif any key or value isnull. - No duplicate keys:
IllegalArgumentExceptionat creation time. - Iteration order not guaranteed.
Map.of("a", 1, "a", 2); // IllegalArgumentException — duplicate key
Map.of(null, 1); // NullPointerException — null key not allowed
Map.of("a", null); // NullPointerException — null value not allowed
Map.ofEntries() — More Than 10 Pairs
Map.of() is limited to 10 key-value pairs. For more, use Map.ofEntries():
import static java.util.Map.entry;
Map<String, String> countryCodes = Map.ofEntries(
entry("GB", "United Kingdom"),
entry("US", "United States"),
entry("DE", "Germany"),
entry("FR", "France"),
entry("JP", "Japan"),
entry("CN", "China"),
entry("IN", "India"),
entry("BR", "Brazil"),
entry("AU", "Australia"),
entry("CA", "Canada"),
entry("RU", "Russia"), // 11th pair — not possible with Map.of()
entry("ZA", "South Africa")
);
Map.entry(K, V) creates an immutable Map.Entry. The static import makes the syntax clean.
copyOf() — Defensive Copies (Java 10)
Java 10 added List.copyOf(), Set.copyOf(), and Map.copyOf() which create immutable copies of existing collections:
// Defensive copy: accept a list but store an immutable snapshot
public class Config {
private final List<String> allowedHosts;
public Config(List<String> hosts) {
this.allowedHosts = List.copyOf(hosts); // immutable snapshot
}
public List<String> getAllowedHosts() {
return allowedHosts; // safe to return — caller cannot mutate it
}
}
// Usage
var hosts = new ArrayList<>(List.of("host1", "host2"));
var config = new Config(hosts);
hosts.add("malicious-host"); // modifying original list
config.getAllowedHosts(); // still ["host1", "host2"]
copyOf() does not copy if the input is already an unmodifiable list created by List.of() or copyOf() — it returns the same instance for efficiency.
Collectors.toUnmodifiableList/Set/Map (Java 10)
These collectors integrate the immutable collection pattern into streams:
// Java 8 — mutable result
var names = users.stream()
.map(User::getName)
.collect(Collectors.toList()); // mutable ArrayList
// Java 10 — immutable result
var names = users.stream()
.map(User::getName)
.collect(Collectors.toUnmodifiableList());
var lookup = users.stream()
.collect(Collectors.toUnmodifiableMap(
User::getId,
User::getName
));
Common Patterns
Constants and configuration
public class AppConstants {
public static final Set<String> SUPPORTED_METHODS =
Set.of("GET", "POST", "PUT", "DELETE", "PATCH");
public static final Map<Integer, String> HTTP_REASONS = Map.ofEntries(
entry(200, "OK"),
entry(201, "Created"),
entry(204, "No Content"),
entry(400, "Bad Request"),
entry(401, "Unauthorized"),
entry(403, "Forbidden"),
entry(404, "Not Found"),
entry(500, "Internal Server Error")
);
}
Test data setup
@Test
void shouldFilterAdults() {
var users = List.of(
new User("Alice", 25),
new User("Bob", 16),
new User("Charlie", 30)
);
var adults = users.stream()
.filter(u -> u.getAge() >= 18)
.collect(Collectors.toList());
assertEquals(List.of("Alice", "Charlie"),
adults.stream().map(User::getName).collect(Collectors.toList()));
}
Lookup table
var priorityMap = Map.of(
"CRITICAL", 1,
"HIGH", 2,
"MEDIUM", 3,
"LOW", 4
);
var sorted = tickets.stream()
.sorted(Comparator.comparingInt(t ->
priorityMap.getOrDefault(t.getPriority(), Integer.MAX_VALUE)))
.collect(Collectors.toList());
Comparison: Factory Methods vs Alternatives
List.of() | Arrays.asList() | new ArrayList<>() | Collections.unmodifiableList() | |
|---|---|---|---|---|
| Immutable | Yes (fully) | No (set() works) | No | Yes (wrapper) |
| Null elements | No | Yes | Yes | Depends on wrapped list |
| Memory | Compact | Array wrapper | Array + capacity | Wrapper + original |
| Use case | Constants, args | Fixed-size mutable | General mutable | Defensive immutable copy |