1. Introduction
Classic collections: flexibility and pitfalls
When you create a collection with new ArrayList<>(), you get a structure you can freely modify: add, remove, change elements. This is convenient when you build data “on the fly.” But what if you pass this collection to another class or method where it must not be modified? And what if you accidentally expose this collection and someone changes it? That’s where problems begin.
A classic mistake
import java.util.*;
public class Example {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// Pass the collection "outside"
processNames(names);
// We expect the list to be unchanged...
System.out.println(names);
}
public static void processNames(List<String> list) {
// And someone went ahead and removed an element!
list.remove("Bob");
}
}
Output:
[Alice, Charlie]
Your collection changed even though you did not plan it at all. In large projects such “surprises” can easily turn into very unpleasant and hard-to-track bugs.
The danger is that code working with the collection can suddenly face unpredictable data changes. There is also always a risk of data loss — someone accidentally removed an element or overwrote it. And if the collection is modified concurrently from different threads, you might hit not only ConcurrentModificationException but also an even more insidious issue — inconsistent data.
Protecting collections: the old approach
Before Java 9, you had to use wrapper methods like Collections.unmodifiableList(...), which we discussed in the previous level. They help return a “frozen” collection. But this approach isn’t always convenient and doesn’t solve all problems (more on that in the next lecture).
2. The modern solution: factory methods List.of, Set.of, Map.of
Java 9 introduced new static methods on collection interfaces: List.of, Set.of, Map.of. They let you quickly and conveniently create a collection that cannot be modified. It’s as if you made a collection and immediately set it in concrete — no one can add, remove, or change elements.
Creating immutable collections — example
import java.util.*;
public class ImmutableDemo {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
Set<Integer> numbers = Set.of(1, 2, 3);
Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25, "Charlie", 28);
System.out.println(names);
System.out.println(numbers);
System.out.println(ages);
}
}
Output:
[Alice, Bob, Charlie]
[1, 2, 3]
{Alice=30, Bob=25, Charlie=28}
How does it work?
- List.of(...) — creates an immutable list.
- Set.of(...) — creates an immutable set.
- Map.of(...) — creates an immutable map (up to 10 key–value pairs; for more, use Map.ofEntries(...)).
Attention! Collections created by these methods do not allow modifications. Any attempt to add, remove, or replace an element will throw an exception.
3. Usage examples and pitfalls
Example: attempting to modify a collection
import java.util.*;
public class ImmutableFail {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob");
// names.add("Charlie"); // Runtime error!
try {
names.add("Charlie");
} catch (UnsupportedOperationException ex) {
System.out.println("Cannot add element: " + ex.getClass().getSimpleName());
}
}
}
Output:
Cannot add element: UnsupportedOperationException
Example: attempting to add null
import java.util.*;
public class NullFail {
public static void main(String[] args) {
try {
List<String> badList = List.of("Alice", null, "Bob");
} catch (NullPointerException ex) {
System.out.println("Null is not allowed: " + ex.getClass().getSimpleName());
}
}
}
Output:
Null is not allowed: NullPointerException
Example: duplicates in Set.of
import java.util.*;
public class DuplicatesFail {
public static void main(String[] args) {
try {
Set<String> badSet = Set.of("one", "two", "one");
} catch (IllegalArgumentException ex) {
System.out.println("Duplicates are not allowed: " + ex.getClass().getSimpleName());
}
}
}
Output:
Duplicates are not allowed: IllegalArgumentException
Example: Map.of with many pairs
import java.util.*;
public class MapOfLarge {
public static void main(String[] args) {
// Map.of supports up to 10 key–value pairs
Map<String, Integer> map = Map.of(
"one", 1, "two", 2, "three", 3, "four", 4, "five", 5,
"six", 6, "seven", 7, "eight", 8, "nine", 9, "ten", 10
);
System.out.println(map);
// For more, use Map.ofEntries
Map<String, Integer> bigMap = Map.ofEntries(
Map.entry("eleven", 11),
Map.entry("twelve", 12),
Map.entry("thirteen", 13)
// ... and so on
);
System.out.println(bigMap);
}
}
4. Features and limitations of immutable collections
No modifications.
Any attempt to add, remove, or change an element will result in UnsupportedOperationException. Even methods that are normally available (add, remove, set) won’t work.
No nulls.
If you try to add null as a list or set element, or as a key/value into a map, you’ll get a NullPointerException. This is for safety: null elements often lead to errors in collections.
No concrete implementation guaranteed.
You won’t know the actual class behind a collection created via List.of and others. Don’t do instanceof ArrayList or try to cast the collection to a specific type.
Element order.
— With List.of, element order is preserved (as in a regular list).
— With Set.of, order is not guaranteed (in practice it may match the argument order, but don’t rely on it).
— With Map.of, pair order is not guaranteed.
Performance.
Collections created via factory methods typically perform better than wrappers over mutable collections because they don’t spend memory on extra capabilities.
5. When and why to use immutable collections
Constant data sets
If you have a list, set, or map that must not change during program execution, use List.of, Set.of, Map.of. For example:
private static final List<String> ROLES = List.of("USER", "ADMIN", "MODERATOR");
Now no one can add an extra role to this list.
Returning collections from methods
If you return a collection from a method and don’t want it to be modified by callers:
public List<String> getDefaultNames() {
return List.of("Alice", "Bob", "Charlie");
}
The recipient won’t be able to corrupt your data.
Passing between application layers
When you pass collections between different parts of a program (for example, between Controller and Service layers in a web application), it’s better to use immutable collections so that no one can silently modify them.
Safety and thread safety
Immutable collections are, by definition, thread-safe for reads: if no one can modify them, you can safely use them from multiple threads without synchronization.
6. Practical examples for a general application
Suppose our training app has a list of supported commands:
public class Commands {
public static final List<String> SUPPORTED_COMMANDS = List.of(
"help", "exit", "list", "add", "remove"
);
}
If you try to do this:
Commands.SUPPORTED_COMMANDS.add("hack_the_system");
You’ll get an exception and won’t be able to harm the application.
Or, for example, if you have a map with error codes:
public class ErrorCodes {
public static final Map<Integer, String> CODES = Map.of(
404, "Not Found",
500, "Internal Server Error",
403, "Forbidden"
);
}
Any attempt to add a new code will throw an exception.
7. Comparing approaches to creating collections
| Creation method | Mutable? | Null allowed? | Duplicates? | Thread-safety | Example |
|---|---|---|---|---|---|
|
Yes | Yes | Yes | No | |
|
No | No | Yes | Yes* | |
|
No | No | No | Yes* | |
|
No | No | No | Yes* | |
|
No | Depends on the source | Yes | No | |
* — thread-safety only in terms of immutability: if no one modifies the collection, it can be safely read from multiple threads.
8. Common mistakes when working with List.of, Set.of, Map.of
Error #1: attempting to modify the collection.
A very common mistake is trying to add or remove an element from a collection created via List.of, Set.of, or Map.of. For example, names.add("Dmitry") or ages.remove("Bob"). This will always result in a UnsupportedOperationException at runtime.
Error #2: attempting to add null.
If you accidentally pass null to any of the methods (for example, List.of("Alice", null)), you’ll get a NullPointerException. Java 9+ immutable collections don’t like null — and that’s actually a good thing.
Error #3: duplicate elements in Set.of or Map.of.
Set.of("a", "b", "a") or Map.of("x", 1, "x", 2) will result in an IllegalArgumentException. A set and a map, by definition, cannot contain duplicates.
Error #4: expecting a specific implementation.
Don’t do this:
List<String> list = List.of("a", "b");
if (list instanceof ArrayList) {
// ...
} // This is always false!
The internal implementation is hidden — don’t rely on implementation details.
Error #5: attempting to use mutating methods.
Even methods like clear(), set(index, value) (on a list) will throw exceptions. Remember: collections created via the factory methods are immutable.
GO TO FULL VERSION