Add Elements to an Immutable List
1. Introduction
In Java, immutable objects are designed to have their state remain unchanged throughout their lifetime to ensure thread safety and prevent unintended modifications, fostering robust and reliable code. However, there are scenarios where applications need to create a modified version of an immutable object. In this example, I’ll demonstrate how to modify an immutable list by adding elements.
Here are some common use cases that need to modify immutable objects:
Use Case | Description |
Thread Safety | In concurrent environments, we may need to create a new immutable object with updated data when certain changes are applied atomically. |
Caching | Need to create a new immutable object with updated data and replace the old cached object with the new one when a cache eviction or refresh occurs. |
Value Objects | Need to create new objects with the results of the operations to avoid side effects when performing operations on value objects such as dates, currencies, and measurements. |
Functional programming | Java functions typically create new immutable objects as output based on input parameters, rather than modifying existing objects. e.g. Streams API. |
2. Set up Maven Project
In this step, I will set up a Java maven project which demonstrates immutable list add element via Junit tests.
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.zheng</groupId> <artifactId>Immutable-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.10.2</version> <scope>test</scope> </dependency> </dependencies> </project>
I will create a TestAddElementToImmutaleList
class with several tests to show how to create a new list from an immutable list and convert the modified list into a new immutable list. Each method will be explained at step 3 and step 4.
TestAddElementToImmutaleList.java
package org.zheng.demo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; class TestAddElementToImmutaleList { final List<Integer> oldImmutableList = List.of(1, 2); List<Integer> newImmutableList; @Test void test_addAll_after_new_arrayList() { final List<Integer> newList = new ArrayList<>(); newList.addAll(oldImmutableList); newList.add(3); assertEquals(3, newList.size(), "newList should have 3 elements after add"); createImmutableList(newList); } private void createImmutableList(final List<Integer> newList) { newImmutableList = newList.stream().toList(); newImmutableList.stream().forEach(n -> System.out.print(n + " ")); System.out.println("***"); } @Test void test_add_after_collector_toList() { final List<Integer> newList = oldImmutableList.stream().collect(Collectors.toList()); newList.add(3); assertEquals(3, newList.size(), "newList should have 3 elements after add"); createImmutableList(newList); } @Test void test_add_after_ArrayList_constructor() { final List<Integer> newList = new ArrayList<>(oldImmutableList); assertEquals(2, newList.size(), "newList should have 2 elements"); newList.add(3); assertEquals(3, newList.size(), "newList should have 3 elements after add"); createImmutableList(newList); } @Test void test_add_after_toArray() { Integer[] newArray = oldImmutableList.toArray(new Integer[3]); final List<Integer> newList = Arrays.asList(newArray); newList.set(2, 3); assertEquals(3, newList.size(), "newList should have 3 elements"); createImmutableList(newList); } @Test void test_modify_immutable_throw_exception() { assertThrows(UnsupportedOperationException.class, () -> { oldImmutableList.set(0, 4); }, "should throw exception"); final List<Integer> newList = oldImmutableList.stream().toList(); assertThrows(UnsupportedOperationException.class, () -> { newList.add(2); }, "should throw exception"); } }
3. Create an Immutable List
In this step, I will create an immutable list from the Stream.toList
method.
3.1 Create an Immutable List
In this step, I will create an immutable list – immutableList
– with the List.of
method which returns an unmodifiable list containing two integers: 1 and 2. I will also define a newImmutableList
which will be used in later steps to add one more integer to contain 3 integers: 1, 2, and 3.
define immutableList and newImmutableList
final List<Integer> oldImmutableList= List.of(1, 2); List<Integer> newImmutableList;
Note: the immutable list can NOT be modified by changing the existing value nor adding new values. You can verify with the following test.
Can not modify immutableList
@Test void test_modify_immutable_throw_exception() { assertThrows(UnsupportedOperationException.class, () -> { immutableList.set(0, 4); }, "should throw exception"); List<Integer> newList = immutableList.stream().toList(); assertThrows(UnsupportedOperationException.class, () -> { newList.add(2); }, "should throw exception"); }
3.2 Create an Immutable List via Stream.toList
In this step, I will use the Stream.toList
method to return an immutable list. It is used to assign the newImmutableList
variable created at step 3.1.
create an immutable List
private void createImmutableList(final List<Integer> newList) { newImmutableList = newList.stream().toList(); newImmutableList.stream().forEach(n -> System.out.print(n + " ")); }
- line 2: create a new immutable list from is a mutable
newList
. - line 4: print out the
newImmutableList
values.
4. Modify an Immutable List
In this step, I will demonstrate immutable list add element with three steps:
- create a new mutable list by copying from the immutable list.
- add new elements to the mutable list.
- create a new immutable list by calling the
createImmutableList
created at step 3.2.
4.1 Create a list via ArrayList
I will add one element after constructing a new mutable ArrayList
.
test_addAll_after_new_arrayList
@Test void test_addAll_after_new_arrayList() { final List<Integer> newList = new ArrayList<>(); newList.addAll(oldImmutableList); newList.add(3); assertEquals(3, newList.size(), "newList should have 3 elements after add"); createImmutableList(newList); }
- line 3: create a new mutable list via the default
ArrayList
constructor. - line 4: utilize the
addAll
method to copy theoldImmutableList
elements to thenewList
. - line 5: add an element to the
newList
. - line 8: call
createImmutableList
to return a new immutable list.
4.2 Create List via Arrays
I will add one element after constructing a new mutable list from Arrays.asList
.
test_add_after_toArray
@Test void test_add_after_toArray() { Integer[] newArray = oldImmutableList.toArray(new Integer[3]); final List<Integer> newList = Arrays.asList(newArray); newList.set(2, 3); assertEquals(3, newList.size(), "newList should have 3 elements"); createImmutableList(newList); }
- line 3: create a new
Integer
Array
with size 3, one bigger than theoldImmutableList.size
. - line 4: create a new list from
Arrays.asList
. - line 6: update the
newList
to set the third element value to 3.
4.3 Create List via ArrayList Copy Constructor
I will add one element after constructing a new mutable ArrayList
.
test_add_after_ArrayList_constructor
@Test void test_add_after_ArrayList_constructor() { final List<Integer> newList = new ArrayList<>(oldImmutableList); assertEquals(2, newList.size(), "newList should have 2 elements"); newList.add(3); assertEquals(3, newList.size(), "newList should have 3 elements after add"); createImmutableList(newList); }
- line 3: create a new
ArrayList
with theoldImmutableList
. - line 6: add a new element to the
newlist
.
4.4 Create List via Collectors.toList
I will add one element after constructing a new mutable from Collectors.toList
.
test_add_after_collector_toList
@Test void test_add_after_collector_toList() { final List<Integer> newList = oldImmutableList.stream().collect(Collectors.toList()); newList.add(3); assertEquals(3, newList.size(), "newList should have 3 elements after add"); createImmutableList(newList); }
- line 3: create a
newList
from theCollectors.toList
method. - line 5: add a new element to the
newList
.
5. Conclusion
In this example, I demonstrated how to modify an immutable list with three steps:
- create a new mutable list by copying from an immutable list.
- add new elements to the mutable list.
- return an immutable list via the
Stream.toList
method.
Here is the Junit test execution screenshot. As you see in the Console log, the new immutable list has three integers: 1, 2, and 3.
6. Download
This was an example of an immutable list add element in Java.
You can download the full source code of this example here: Add Elements to an Immutable List