Immutable collections are famous due to several advantages like thread safety, functional programming, cacheable and more. If you are not familiar with Immutable collections, you can read the Immutable collections in Java from here. But How should we create an immutable collection in Java? Which is the best way to create an immutable collection in Java?
Most of the Immutable collections are data structures that can’t be modified once they are created. Immutability is an essential property in multi-threaded environments and helps in creating robust and predictable software. Let’s see a simple example of immutable collections:
import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; public class ImmutableCollectionsExample { public static void main(String[] args) { // ArrayList List<String> fruitList = new ArrayList<>(); fruitList.add("Apple"); fruitList.add("Banana"); // Creating an unmodifiable list List<String> unmodifiableFruitList = Collections.unmodifiableList(fruitList); try { unmodifiableFruitList.add("Cherry"); } catch (UnsupportedOperationException e) { System.out.println("Cannot modify the ArrayList"); } // HashSet Set<String> fruitSet = new HashSet<>(); fruitSet.add("Apple"); fruitSet.add("Banana"); // Creating an unmodifiable set Set<String> unmodifiableFruitSet = Collections.unmodifiableSet(fruitSet); try { unmodifiableFruitSet.add("Cherry"); } catch (UnsupportedOperationException e) { System.out.println("Cannot modify the HashSet"); } // HashMap Map<String, Integer> fruitMap = new HashMap<>(); fruitMap.put("Apple", 1); fruitMap.put("Banana", 2); // Creating an unmodifiable map Map<String, Integer> unmodifiableFruitMap = Collections.unmodifiableMap(fruitMap); try { unmodifiableFruitMap.put("Cherry", 3); } catch (UnsupportedOperationException e) { System.out.println("Cannot modify the HashMap"); } } }
Output: Cannot modify the ArrayList
Cannot modify the HashSet
Cannot modify the HashMap
Here, we are creating mutable collections, like ArrayList, HashSet, and HashMap collections, and after we fill them with some sample data. Then we use Collections.unmodifiableList, Collections.unmodifiableSet, and Collections.unmodifiableMap to create unmodifiable (read-only) views of these collections.
Note: Remember that is one of the ways to create an immutable collection. In this post, we will see different ways to immutable collection and see which one the best way is to create.
Finally, we try to modify these read-only collections, which results in UnsupportedOperationException being thrown, thus confirming their immutability.
Note: These collections are not deeply immutable, If the elements are mutable objects, their internal state can still be altered. I will explain it soon but before that let’s see how many ways to create an immutable collection.
In this post, we will discuss three ways to create an immutable collection and then see how should we create an immutable collection in Java.
1. Collections.unmodifiable* methods
2. List.of() methods
3. CopyOf() methods
1. Collections.unmodifiable* method in java
Collections class has some methods that are used to create unmodifiable collections (not strictly immutable). These methods take a mutable collection as an argument and return an unmodifiable view of it. For example, we will show you different methods to create different types of collections.
So, let’s create an example where we will see how to create unmodifiable collections. Here we are creating a simple Student class and modifiable collections (Mutable collections). I then make them unmodifiable using Collections.unmodifiableList, Collections.unmodifiableSet, and Collections.unmodifiableMap.
public class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return name + "(" + age + ")"; } }
import java.util.*; public class UnmodifiableCollectionsWithStudentExample { public static void main(String[] args) { // Create modifiable List, Set, and Map of Student objects List<Student> modifiableList = new ArrayList<>(); modifiableList.add(new Student("Alice", 22)); modifiableList.add(new Student("Bob", 20)); Set<Student> modifiableSet = new HashSet<>(); modifiableSet.add(new Student("Charlie", 21)); modifiableSet.add(new Student("David", 23)); Map<String, Student> modifiableMap = new HashMap<>(); modifiableMap.put("Student1", new Student("Eve", 19)); modifiableMap.put("Student2", new Student("Frank", 25)); // Create unmodifiable views of the original collections List<Student> unmodifiableList = Collections.unmodifiableList(modifiableList); Set<Student> unmodifiableSet = Collections.unmodifiableSet(modifiableSet); Map<String, Student> unmodifiableMap = Collections.unmodifiableMap(modifiableMap); // Attempt to modify unmodifiable List. This will throw UnsupportedOperationException. try { unmodifiableList.add(new Student("Grace", 18)); } catch (UnsupportedOperationException e) { System.out.println("Can't modify the unmodifiableList"); } // Attempt to modify unmodifiable Set. This will throw UnsupportedOperationException. try { unmodifiableSet.add(new Student("Helen", 24)); } catch (UnsupportedOperationException e) { System.out.println("Can't modify the unmodifiableSet"); } // Attempt to modify unmodifiable Map. This will throw UnsupportedOperationException. try { unmodifiableMap.put("Student3", new Student("Isaac", 22)); } catch (UnsupportedOperationException e) { System.out.println("Can't modify the unmodifiableMap"); } } }
Output: Can’t modify the unmodifiableList
Can’t modify the unmodifiableSet
Can’t modify the unmodifiableMap
After the creation of the unmodifiable collection, we tried to add a new Student to each unmodifiable collection, which resulted in UnsupportedOperationException as expected.
But before moving further we should see how does it work?
How do unmodifiable collections work?
The Collections.unmodifiable* methods (Like unmodifiableSet, unmodifiableMap, and unmodifiableList) create a read-only view of the original collection. These methods don’t create a separate copy of Collection, they just create a view, and they don’t allocate new memory for the elements. So, both collections (Modifiable and Unmodifiable reference the same objects in memory. If the underlying original collection is modified, the read-only view will reflect those changes. Let’s try it with an example
public class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return name + "(" + age + ")"; } }
import java.util.*; public class UnmodifiableCollectionsWithStudentExample { public static void main(String[] args) { // Create modifiable List, Set, and Map of Student objects List<Student> modifiableList = new ArrayList<>(); modifiableList.add(new Student("Alice", 22)); modifiableList.add(new Student("Bob", 20)); Set<Student> modifiableSet = new HashSet<>(); modifiableSet.add(new Student("Charlie", 21)); modifiableSet.add(new Student("David", 23)); Map<String, Student> modifiableMap = new HashMap<>(); modifiableMap.put("Student1", new Student("Eve", 19)); modifiableMap.put("Student2", new Student("Frank", 25)); // Create unmodifiable views of the original collections List<Student> unmodifiableList = Collections.unmodifiableList(modifiableList); Set<Student> unmodifiableSet = Collections.unmodifiableSet(modifiableSet); Map<String, Student> unmodifiableMap = Collections.unmodifiableMap(modifiableMap); // Attempt to modify unmodifiable List. This will throw UnsupportedOperationException. try { unmodifiableList.add(new Student("Grace", 18)); } catch (UnsupportedOperationException e) { System.out.println("Can't modify the unmodifiableList"); } // Attempt to modify unmodifiable Set. This will throw UnsupportedOperationException. try { unmodifiableSet.add(new Student("Helen", 24)); } catch (UnsupportedOperationException e) { System.out.println("Can't modify the unmodifiableSet"); } // Attempt to modify unmodifiable Map. This will throw UnsupportedOperationException. try { unmodifiableMap.put("Student3", new Student("Isaac", 22)); } catch (UnsupportedOperationException e) { System.out.println("Can't modify the unmodifiableMap"); } // Modify original collections modifiableList.add(new Student("Grace", 18)); modifiableSet.add(new Student("Helen", 24)); modifiableMap.put("Student3", new Student("Isaac", 22)); // Unmodifiable collections will reflect these changes System.out.println("List: " + unmodifiableList); System.out.println("Set: " + unmodifiableSet); System.out.println("Map: " + unmodifiableMap); } }
Output: Can’t modify the unmodifiableList
Can’t modify the unmodifiableSet
Can’t modify the unmodifiableMap
List: [Alice(22), Bob(20), Grace(18)]
Set: [David(23), Helen(24), Charlie(21)]
Map: {Student3=Isaac(22), Student1=Eve(19), Student2=Frank(25)}
2. List.of(), Set.of(), Map.of() methods in java
These methods were introduced in Java 9 to create an immutable collection. By use of these methods, we can directly create instances of List, Set, and Map that are not only unmodifiable but also optimized for smaller sizes. Unlike an unmodifiable collection, we don’t need to create a modifiable collection first.
Let’s create an example and see how to create an immutable collection by use of List.of(), Set.of(), Map.of().
public class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return name + "(" + age + ")"; } }
import java.util.List; import java.util.Set; import java.util.Map; public class ImmutableCollectionsExample { public static void main(String[] args) { // Create an immutable List of Student objects using List.of List<Student> immutableStudentList = List.of( new Student("Alice", 22), new Student("Bob", 20) ); // Create an immutable Set of Student objects using Set.of Set<Student> immutableStudentSet = Set.of( new Student("Charlie", 21), new Student("David", 23) ); // Create an immutable Map of Student objects using Map.of Map<String, Student> immutableStudentMap = Map.of( "Student1", new Student("Eve", 19), "Student2", new Student("Frank", 25) ); // Print the immutable collections System.out.println("Immutable Student List: " + immutableStudentList); System.out.println("Immutable Student Set: " + immutableStudentSet); System.out.println("Immutable Student Map: " + immutableStudentMap); // The following operations would throw UnsupportedOperationException // immutableStudentList.add(new Student("Grace", 18)); // immutableStudentSet.add(new Student("Helen", 24)); // immutableStudentMap.put("Student3", new Student("Isaac", 22)); } }
Output: Immutable Student List: [Alice(22), Bob(20)]
Immutable Student Set: [David(23), Charlie(21)]
Immutable Student Map: {Student1=Eve(19), Student2=Frank(25)}
So, let’s discuss what we did in this example. Here we used List.of, Set.of, and Map.of methods to create immutable collections of Student objects. After the creation of collections, we can’t add, remove, or modify their elements. You can try out this, just uncomment and execute the code and it will throw an UnsupportedOperationException.
Now turn to see how it works in memory.
How does List.of(), Set.of(), Map.of() methods work?
These methods are utility methods that are useful for creating small collections where the elements are known at compile time. As we know Collections.Unmodifiable* doesn’t create a strictly immutable collection but these methods return collections that are not only unmodifiable but also immutable.
3. List.copyOf(), Set.copyOf(), Map.copyOf():
Java 10 introduced some more methods to create unmodifiable collections. These methods don’t create an unmodifiable collection directly, they take a modifiable collection as an argument. Unlike collections.unmodifiable* methods, the collections created by copyOf() methods are separate instances and don’t reflect changes made to the original mutable collections. It’s useful in multi-threaded environments where we want to ensure that a data structure remains constant throughout its lifecycle.
Now it turns to see an example of the unmodifiable collection by use of copyOf() methods.
public class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return name + "(" + age + ")"; } }
import java.util.*; public class ImmutableCollectionsExample { public static void main(String[] args) { // Original mutable List List<Student> originalStudentList = new ArrayList<>(); originalStudentList.add(new Student("Alice", 22)); originalStudentList.add(new Student("Bob", 20)); // Create an unmodifiable List using List.copyOf List<Student> unmodifiableStudentList = List.copyOf(originalStudentList); System.out.println("Unmodifiable Student List: " + unmodifiableStudentList); // Original mutable Set Set<Student> originalStudentSet = new HashSet<>(); originalStudentSet.add(new Student("Charlie", 21)); originalStudentSet.add(new Student("David", 23)); // Create an unmodifiable Set using Set.copyOf Set<Student> unmodifiableStudentSet = Set.copyOf(originalStudentSet); System.out.println("Unmodifiable Student Set: " + unmodifiableStudentSet); // Original mutable Map Map<String, Student> originalStudentMap = new HashMap<>(); originalStudentMap.put("Student1", new Student("Eve", 19)); originalStudentMap.put("Student2", new Student("Frank", 25)); // Create an unmodifiable Map using Map.copyOf Map<String, Student> unmodifiableStudentMap = Map.copyOf(originalStudentMap); System.out.println("Unmodifiable Student Map: " + unmodifiableStudentMap); // This will throw an UnsupportedOperationException try { unmodifiableStudentList.add(new Student("Grace", 24)); } catch (UnsupportedOperationException e) { System.out.println("Unsupported operation for unmodifiable Student List!"); } // This will throw an UnsupportedOperationException try { unmodifiableStudentSet.add(new Student("Grace", 24)); } catch (UnsupportedOperationException e) { System.out.println("Unsupported operation for unmodifiable Student Set!"); } // This will throw an UnsupportedOperationException try { unmodifiableStudentMap.put("Student3", new Student("Grace", 24)); } catch (UnsupportedOperationException e) { System.out.println("Unsupported operation for unmodifiable Student Map!"); } } }
Output: Unmodifiable Student List: [Alice(22), Bob(20)]
Unmodifiable Student Set: [Charlie(21), David(23)]
Unmodifiable Student Map: {Student1=Eve(19), Student2=Frank(25)}
Unsupported operation for unmodifiable Student List!
Unsupported operation for unmodifiable Student Set!
Unsupported operation for unmodifiable Student Map!
Here we create a mutable list (originalStudentList), mutable set(originalStudentSet), mutable map (originalStudentMap). After that, we created an unmodifiable copy of each collection using the copyOf() method and stored it in a new variable.
At the end, we tried to add a new student to each of these unmodifiable collections and it throws UnsupportedOperationException for each modification attempt.
How does copyOf() method work in Memory?
The copyOf() method creates a new collection instance by use of given collections. They don’t share the structural modifications with the original collections. So, if you make any change in the original collections, it doesn’t reflect new collection instances.
import java.util.*; public class ImmutableCollectionsExample { public static void main(String[] args) { // Original mutable List List<Student> originalStudentList = new ArrayList<>(); originalStudentList.add(new Student("Alice", 22)); originalStudentList.add(new Student("Bob", 20)); // Create an unmodifiable List using List.copyOf List<Student> unmodifiableStudentList = List.copyOf(originalStudentList); System.out.println("Unmodifiable Student List: " + unmodifiableStudentList); // Original mutable Set Set<Student> originalStudentSet = new HashSet<>(); originalStudentSet.add(new Student("Charlie", 21)); originalStudentSet.add(new Student("David", 23)); // Create an unmodifiable Set using Set.copyOf Set<Student> unmodifiableStudentSet = Set.copyOf(originalStudentSet); System.out.println("Unmodifiable Student Set: " + unmodifiableStudentSet); // Original mutable Map Map<String, Student> originalStudentMap = new HashMap<>(); originalStudentMap.put("Student1", new Student("Eve", 19)); originalStudentMap.put("Student2", new Student("Frank", 25)); // Create an unmodifiable Map using Map.copyOf Map<String, Student> unmodifiableStudentMap = Map.copyOf(originalStudentMap); System.out.println("Unmodifiable Student Map: " + unmodifiableStudentMap); // Modify the original collections originalStudentList.add(new Student("Zach", 24)); originalStudentSet.add(new Student("George", 26)); originalStudentMap.put("Student3", new Student("Helen", 27)); // Show that the original collections have changed System.out.println("Modified Original Student List: " + originalStudentList); System.out.println("Modified Original Student Set: " + originalStudentSet); System.out.println("Modified Original Student Map: " + originalStudentMap); // Show that the unmodifiable collections remain unchanged System.out.println("Unmodifiable Student List after modification to original: " + unmodifiableStudentList); System.out.println("Unmodifiable Student Set after modification to original: " + unmodifiableStudentSet); System.out.println("Unmodifiable Student Map after modification to original: " + unmodifiableStudentMap); } }
Output: Unmodifiable Student List: [Alice(22), Bob(20)]
Unmodifiable Student Set: [Charlie(21), David(23)]
Unmodifiable Student Map: {Student1=Eve(19), Student2=Frank(25)}
Modified Original Student List: [Alice(22), Bob(20), Zach(24)]
Modified Original Student Set: [David(23), George(26), Charlie(21)]
Modified Original Student Map: {Student3=Helen(27), Student1=Eve(19), Student2=Frank(25)}
Unmodifiable Student List after modification to original: [Alice(22), Bob(20)]
Unmodifiable Student Set after modification to original: [Charlie(21), David(23)]
Unmodifiable Student Map after modification to original: {Student1=Eve(19), Student2=Frank(25)}
NOTE: They don’t deeply clone the objects within the collection, meaning that if the objects themselves are mutable, they can still be modified.
import java.util.*; public class ImmutableCollectionsExample { public static void main(String[] args) { // Create an original mutable list of Student objects Student alice = new Student("Alice", 22); Student bob = new Student("Bob", 20); List<Student> originalStudentList = new ArrayList<>(); originalStudentList.add(alice); originalStudentList.add(bob); // Create an unmodifiable List using List.copyOf List<Student> unmodifiableStudentList = List.copyOf(originalStudentList); System.out.println("Unmodifiable Student List: " + unmodifiableStudentList); // Modify the original Student object alice.setAge(23); // Observe that the unmodifiable list reflects the change System.out.println("Unmodifiable Student List after modifying original Student object: " + unmodifiableStudentList); } }
Output: Unmodifiable Student List: [Alice(22), Bob(20)]
Unmodifiable Student List after modifying original Student object: [Alice(23), Bob(20)]
Now, you must have seen the example and output. The copyOf() methods don’t perform a deep copy of the objects within the collection, so the objects within the collection are still references to the original objects.
In this example, you can see we created a mutable list (originalStudentList) and added two students: Alice and Bob. Then, we created a new list that is unmodifiable (unmodifiableStudentList) by use of copyOf() method. In the end, I tried to modify Alice’s age using the setAge method of the original Student object Alice. Finally, I print out the unmodifiableStudentList to observe that it also reflects this change.
It shows that even though the list itself is unmodifiable, the objects within the list are still references to the original mutable objects, and changes to the original objects are visible in the unmodifiable list.