Skip to content

Commit 9d63366

Browse files
committed
Add Immutable static helper
1 parent 9b9d596 commit 9d63366

2 files changed

Lines changed: 279 additions & 0 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package org.int4.common.collection;
2+
3+
import java.util.AbstractList;
4+
import java.util.ArrayList;
5+
import java.util.Collections;
6+
import java.util.Iterator;
7+
import java.util.List;
8+
import java.util.ListIterator;
9+
import java.util.Objects;
10+
import java.util.RandomAccess;
11+
12+
/**
13+
* Utility for creating immutable representations of common collection types.
14+
* <p>
15+
* This class provides factory methods that accept existing mutable or immutable
16+
* collections and return a logically immutable view of their contents.
17+
* <p>
18+
* Where possible, known immutable implementations are detected and returned
19+
* without copying. Otherwise, a defensive copy is created to ensure isolation
20+
* from subsequent external modifications.
21+
* <p>
22+
* The immutability guarantee applies to the returned collection interface:
23+
* structural modification operations are not supported and will result in
24+
* {@link UnsupportedOperationException}.
25+
* <p>
26+
* Null elements are preserved unless otherwise specified by a particular method.
27+
*/
28+
public abstract class Immutable {
29+
30+
/**
31+
* Returns an immutable representation of the given list.
32+
* <p>
33+
* If the provided list is already a known immutable implementation,
34+
* it is returned as-is without copying.
35+
* <p>
36+
* Otherwise, a defensive copy of the input is created and wrapped in an
37+
* immutable view to prevent external mutation.
38+
* <p>
39+
* The returned list does not permit structural modification operations
40+
* and will throw {@link UnsupportedOperationException} if such methods are
41+
* invoked.
42+
* <p>
43+
* Null elements are preserved.
44+
*
45+
* @param <T> element type
46+
* @param list the source list, must not be {@code null}
47+
* @return an immutable list backed by a defensive copy, or the original list
48+
* if it is already immutable, never {@code null}
49+
* @throws NullPointerException if {@code list} is {@code null}
50+
*/
51+
public static <T> List<T> of(List<T> list) {
52+
Objects.requireNonNull(list, "list");
53+
54+
if(isKnownImmutable(list)) {
55+
return list;
56+
}
57+
58+
return list instanceof RandomAccess
59+
? new RandomAccessImmutableList<>(Collections.unmodifiableList(new ArrayList<>(list)))
60+
: new ImmutableList<>(Collections.unmodifiableList(new ArrayList<>(list)));
61+
}
62+
63+
private static boolean isKnownImmutable(List<?> list) {
64+
return list instanceof RandomAccessImmutableList<?>
65+
|| list instanceof ImmutableList<?>
66+
|| list.getClass().getName().startsWith("java.util.ImmutableCollections$");
67+
}
68+
69+
private static final class RandomAccessImmutableList<T> extends AbstractList<T> implements RandomAccess {
70+
private final List<T> delegate;
71+
72+
private RandomAccessImmutableList(List<T> delegate) {
73+
this.delegate = delegate;
74+
}
75+
76+
@Override
77+
public T get(int index) {
78+
return delegate.get(index);
79+
}
80+
81+
@Override
82+
public int size() {
83+
return delegate.size();
84+
}
85+
86+
@Override
87+
public boolean contains(Object o) {
88+
return delegate.contains(o);
89+
}
90+
91+
@Override
92+
public int indexOf(Object o) {
93+
return delegate.indexOf(o);
94+
}
95+
96+
@Override
97+
public int lastIndexOf(Object o) {
98+
return delegate.lastIndexOf(o);
99+
}
100+
101+
@Override
102+
public Iterator<T> iterator() {
103+
return delegate.iterator();
104+
}
105+
106+
@Override
107+
public ListIterator<T> listIterator(int index) {
108+
return delegate.listIterator(index);
109+
}
110+
}
111+
112+
private static final class ImmutableList<T> extends AbstractList<T> {
113+
private final List<T> delegate;
114+
115+
private ImmutableList(List<T> delegate) {
116+
this.delegate = delegate;
117+
}
118+
119+
@Override
120+
public T get(int index) {
121+
return delegate.get(index);
122+
}
123+
124+
@Override
125+
public int size() {
126+
return delegate.size();
127+
}
128+
129+
@Override
130+
public boolean contains(Object o) {
131+
return delegate.contains(o);
132+
}
133+
134+
@Override
135+
public int indexOf(Object o) {
136+
return delegate.indexOf(o);
137+
}
138+
139+
@Override
140+
public int lastIndexOf(Object o) {
141+
return delegate.lastIndexOf(o);
142+
}
143+
144+
@Override
145+
public Iterator<T> iterator() {
146+
return delegate.iterator();
147+
}
148+
149+
@Override
150+
public ListIterator<T> listIterator(int index) {
151+
return delegate.listIterator(index);
152+
}
153+
}
154+
155+
private Immutable() {
156+
}
157+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2025 John Hendrikx
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package org.int4.common.collection;
26+
27+
import java.util.ArrayList;
28+
import java.util.Arrays;
29+
import java.util.LinkedList;
30+
import java.util.List;
31+
import java.util.RandomAccess;
32+
33+
import org.junit.jupiter.api.Test;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
37+
38+
class ImmutableTest {
39+
40+
@Test
41+
void shouldThrowExceptionWhenListIsNull() {
42+
assertThatThrownBy(() -> Immutable.of(null))
43+
.isExactlyInstanceOf(NullPointerException.class)
44+
.hasMessage("list");
45+
}
46+
47+
@Test
48+
void shouldCreateImmutableCopyOfMutableList() {
49+
List<String> mutableList = new ArrayList<>(Arrays.asList("a", "b", "c"));
50+
List<String> immutableList = Immutable.of(mutableList);
51+
52+
assertThat(immutableList).containsExactly("a", "b", "c");
53+
54+
mutableList.add("d");
55+
56+
assertThat(immutableList).containsExactly("a", "b", "c");
57+
assertThat(immutableList).hasSize(3);
58+
}
59+
60+
@Test
61+
void shouldNotAllowModifications() {
62+
List<String> immutableList = Immutable.of(Arrays.asList("a", "b"));
63+
64+
assertThatThrownBy(() -> immutableList.add("c")).isExactlyInstanceOf(UnsupportedOperationException.class);
65+
assertThatThrownBy(() -> immutableList.remove(0)).isExactlyInstanceOf(UnsupportedOperationException.class);
66+
assertThatThrownBy(() -> immutableList.set(0, "z")).isExactlyInstanceOf(UnsupportedOperationException.class);
67+
assertThatThrownBy(() -> immutableList.clear()).isExactlyInstanceOf(UnsupportedOperationException.class);
68+
}
69+
70+
@Test
71+
void shouldReturnSameInstanceIfAlreadyImmutable() {
72+
List<String> original = List.of("a", "b");
73+
List<String> immutable = Immutable.of(original);
74+
75+
assertThat(immutable).isSameAs(original);
76+
77+
List<String> doubleImmutable = Immutable.of(immutable);
78+
79+
assertThat(doubleImmutable).isSameAs(immutable);
80+
}
81+
82+
@Test
83+
void shouldPreserveRandomAccess() {
84+
List<String> arrayList = new ArrayList<>(Arrays.asList("a", "b"));
85+
List<String> immutableArrayList = Immutable.of(arrayList);
86+
87+
assertThat(immutableArrayList).isInstanceOf(RandomAccess.class);
88+
assertThat(immutableArrayList.getClass().getSimpleName()).contains("RandomAccess");
89+
90+
List<String> linkedList = new LinkedList<>(Arrays.asList("a", "b"));
91+
List<String> immutableLinkedList = Immutable.of(linkedList);
92+
93+
assertThat(immutableLinkedList).isNotInstanceOf(RandomAccess.class);
94+
assertThat(immutableLinkedList.getClass().getSimpleName()).doesNotContain("RandomAccess");
95+
}
96+
97+
@Test
98+
void shouldPreserveNulls() {
99+
List<String> listWithNulls = Arrays.asList("a", null, "c");
100+
List<String> immutable = Immutable.of(listWithNulls);
101+
102+
assertThat(immutable).containsExactly("a", null, "c");
103+
assertThat(immutable.get(1)).isNull();
104+
assertThat(immutable.contains(null)).isTrue();
105+
assertThat(immutable.indexOf(null)).isEqualTo(1);
106+
assertThat(immutable.lastIndexOf(null)).isEqualTo(1);
107+
}
108+
109+
@Test
110+
void shouldSupportBasicListOperations() {
111+
List<String> list = Immutable.of(Arrays.asList("a", "b", "a"));
112+
113+
assertThat(list.get(0)).isEqualTo("a");
114+
assertThat(list.size()).isEqualTo(3);
115+
assertThat(list.contains("b")).isTrue();
116+
assertThat(list.contains("c")).isFalse();
117+
assertThat(list.indexOf("a")).isEqualTo(0);
118+
assertThat(list.lastIndexOf("a")).isEqualTo(2);
119+
assertThat(list.iterator()).toIterable().containsExactly("a", "b", "a");
120+
assertThat(list.listIterator(1)).toIterable().containsExactly("b", "a");
121+
}
122+
}

0 commit comments

Comments
 (0)