배경
불변 컬렉션을 생성하려는데 Collections.unmodifiableList(), ImmutableList.of() 두 가지 방법이 있어서 정리한다.
요약
- Collections.unmodifiableList(), ImmutableList.of()로 생성된 컬렉션은 모두 불변성을 보장한다.
- Collections.unmodifiableList(origin)는 원본이 변경될 경우 생성된 컬렉션도 영향받는다.
- ImmutableList.of() 생성 시점에 원본을 복사하므로 원본이 변경되어도 영향받지 않는다.
- 따라서 가장 안전한 방법은 ImmutableList이다. (단, 내부적으로 원본을 복사하므로 메모리를 조금 더 사용할 수 있다.)
- Java9부턴 List.of()가 ImmutableList.of()를 대체할 수 있다.
내용
불변 컬렉션
불변 컬렉션(Immutable Collection)은 컬렉션의 불변성을 보장하며 element를 추가/삭제할 수 없다.
단, 불변성은 컬렉션에만 해당하며 element의 참조나 내부 값은 변경할 수 있다.
불변 컬렉션의 장점
불변성을 보장하므로 multi-thread 환경에서 thead 간 동기화를 걱정하지 않으면서 공유하여 사용할 수 있다.
비즈니스 로직에서도 해당 객체를 부담 없이 사용할 수 있다.
불변 컬렉션 생성 방법
Collections.unmodifiableList()
core Java에서 제공 (java.util.Collections)
List<Integer> originUserIds = new ArrayList<>();
originUserIds.add(1);
originUserIds.add(2);
List<Integer> unmodifiableUserIds = Collections.unmodifiableList(originUserIds);
ImmutableList.of()
google Guava에서 제공 (com.google.common.collect.ImmutableList)
List<Integer> originUserIds = new ArrayList<>();
originUserIds.add(1);
originUserIds.add(2);
List<Integer> immutableUserIds = ImmutableList.copyOf(originUserIds);
// 또는 List<Integer> userIds = ImmutableList.of(1, 2);
List.of()
Java9 이후로 List.of() 정적 팩토리 메서드로 불변 객체를 만들 수 있다. (java.util.List)
List<Integer> userIds = List.of(1, 2);
불변 컬렉션 비교 (공통점)
세 컬렉션 모두 수정 연산은 지원하지 않으며 호출 시 UnsupportedOperationException 이 발생한다.
Collections.unmodifiableList()
public class Collections {
...
static class UnmodifiableList<E> extends UnmodifiableCollection<E> implements List<E> {
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
...
}
ImmutableList.of()
public abstract class ImmutableList<E> extends ImmutableCollection<E> implements List<E>, RandomAccess {
...
@Override
@DoNotCall("Always throws UnsupportedOperationException")
public final void add(int index, E element) {
throw new UnsupportedOperationException();
}
...
}
List.of()
public interface List<E> extends Collection<E> {
...
@jdk.internal.ValueBased
static abstract class AbstractImmutableCollection<E> extends AbstractCollection<E> {
// all mutating methods throw UnsupportedOperationException
@Override public boolean add(E e) { throw uoe(); }
@Override public boolean addAll(Collection<? extends E> c) { throw uoe(); }
@Override public void clear() { throw uoe(); }
@Override public boolean remove(Object o) { throw uoe(); }
@Override public boolean removeAll(Collection<?> c) { throw uoe(); }
@Override public boolean removeIf(Predicate<? super E> filter) { throw uoe(); }
@Override public boolean retainAll(Collection<?> c) { throw uoe(); }
}
...
}
불변 컬렉션 비교 (차이점)
Collections.unmodifiableList()
unmodifiableList는 원본을 wrapping 만 하기 때문에 원본의 변경에 영향받는다.
originUserIds.add(3);
originUserIds.size(); // 4
unmodifiableUserIds.size(); // 4
내부 코드를 보면 원본을 받아서 그대로 필드로 사용하고 UnmodifiableList 타입만 바꾸기 때문이다.
public static<T> List<T> unmodifiableList(List<?extends T> list){
if(list.getClass()==UnmodifiableList.class ||list.getClass()==UnmodifiableRandomAccessList.class){
return(List<T>)list;
}
return(list instanceof RandomAccess?
new UnmodifiableRandomAccessList<>(list):
new UnmodifiableList<>(list));
}
static class UnmodifiableList<E> extends UnmodifiableCollection<E> implements List<E> {
@SuppressWarnings("serial") // Conditionally serializable
final List<? extends E> list;
UnmodifiableList(List<? extends E> list) {
super(list);
this.list = list;
}
ImmutableList.of()
ImmutableList는 원본을 copy 하기 때문에 원본의 변경에 영향받지 않는다.
public abstract class ImmutableList<E> extends ImmutableCollection<E> implements List<E>, RandomAccess {
...
public static <E> ImmutableList<E> of(E e1, E e2) {
return construct(e1, e2);
}
private static <E> ImmutableList<E> construct(Object... elements) {
return asImmutableList(checkElementsNotNull(elements));
}
static <E> ImmutableList<E> asImmutableList(Object[] elements) {
return asImmutableList(elements, elements.length);
}
/**
* Views the array as an immutable list. Copies if the specified range does not cover the complete
* array. Does not check for nulls.
*/
static <E> ImmutableList<E> asImmutableList(@Nullable Object[] elements, int length) {
switch (length) {
case 0:
return of();
case 1:
/*
* requireNonNull is safe because the callers promise to put non-null objects in the first
* `length` array elements.
*/
@SuppressWarnings("unchecked") // our callers put only E instances into the array
E onlyElement = (E)requireNonNull(elements[0]);
return of(onlyElement);
default:
/*
* The suppression is safe because the callers promise to put non-null objects in the first
* `length` array elements.
*/
@SuppressWarnings("nullness")
Object[] elementsWithoutTrailingNulls =
length < elements.length ? Arrays.copyOf(elements, length) : elements;
return new RegularImmutableList<E>(elementsWithoutTrailingNulls);
}
}
...
}
List.of()
List 도 of, copyOf 정적 팩토리 메서드에서 원본을 copy 하기 때문에 원본의 변경에 영향받지 않는다.
public interface List<E> extends Collection<E> {
...
static <E> List<E> copyOf(Collection<? extends E> coll) {
return ImmutableCollections.listCopy(coll);
}
static <E> List<E> listCopy(Collection<? extends E> coll) {
if (coll instanceof List12 || (coll instanceof ListN && ! ((ListN<?>)coll).allowNulls)) {
return (List<E>)coll;
} else {
return (List<E>)List.of(coll.toArray()); // implicit nullcheck of coll
}
}
static <E> List<E> of(E... elements) {
switch (elements.length) { // implicit null check of elements
case 0:
@SuppressWarnings("unchecked")
var list = (List<E>) ImmutableCollections.EMPTY_LIST;
return list;
case 1:
return new ImmutableCollections.List12<>(elements[0]);
case 2:
return new ImmutableCollections.List12<>(elements[0], elements[1]);
default:
return ImmutableCollections.listFromArray(elements);
}
}
static <E> List<E> listFromArray(E... input) {
// copy and check manually to avoid TOCTOU
@SuppressWarnings("unchecked")
E[] tmp = (E[])new Object[input.length]; // implicit nullcheck of input
for (int i = 0; i < input.length; i++) {
tmp[i] = Objects.requireNonNull(input[i]);
}
return new ListN<>(tmp, false);
}
...
}
참고
Immutable vs Unmodifiable collection
From the Collections Framework Overview: Collections that do not support modification operations (such as add, remove and clear) are referred to as unmodifiable. Collections that are not
stackoverflow.com
Immutable Set in Java | Baeldung
Learn about different ways to create an immutable set in Java
www.baeldung.com
GitHub - google/guava: Google core libraries for Java
Google core libraries for Java. Contribute to google/guava development by creating an account on GitHub.
github.com