Core Java

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 CaseDescription
Thread SafetyIn concurrent environments, we may need to create a new immutable object with updated data when certain changes are applied atomically.
CachingNeed 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 ObjectsNeed 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 programmingJava functions typically create new immutable objects as output based on input parameters, rather than modifying existing objects. e.g. Streams API.
Table 1. Modify Immutable Objects Use Cases

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 the oldImmutableList elements to the newList.
  • 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 the oldImmutableList.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 the oldImmutableList.
  • 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 the Collectors.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.

Figure 1. Junit Test Results

6. Download

This was an example of an immutable list add element in Java.

Download
You can download the full source code of this example here: Add Elements to an Immutable List

Mary Zheng

Mary graduated from the Mechanical Engineering department at ShangHai JiaoTong University. She also holds a Master degree in Computer Science from Webster University. During her studies she has been involved with a large number of projects ranging from programming and software engineering. She worked as a lead Software Engineer in the telecommunications sector where she led and worked with others to design, implement, and monitor the software solution.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button