항상 고정된 값을 저장하는 Collection이 필요한 경우 또는 Thread-safe한 코드를 짜기 위해서 한번 선언할 때의 값을 쭉 유지하기 위해 외부에서 변경할 수 없는 Collection을 만들어야 할 때가 있다.
한번 Collection을 선언해놓으면 값의 추가, 삭제, 변경이 안되는 Collection은 어떻게 만들 수 있을까?
결론부터 말하면 Collection을 선언할 때 final을 붙이더라도 컬렉션은 요소의 추가, 삭제, 변경이 가능하다.
컬렉션과 같은 참조 자료형 변수는 변수 안에 참조하고 있는 객체의 주소값이 저장되게 된다. 즉, Collection의 요소를 추가, 변경, 삭제해도 그 주소값은 바뀌지 않기 때문에 final을 붙인다고 해서 그 컬렉션의 요소들을 변하지 않게 해주는 것은 아니다.
아래 예제를 살펴보자.
import java.util.*;
import java.io.*;
class Main {
static final List<Integer> final_list = new ArrayList<>(Arrays.asList(1, 2, 3));
static final List<Integer> unmodifiable_list = Collections.unmodifiableList(final_list);
public static void main(String[] args) {
System.out.println("변경 전: " + final_list);
System.out.println(unmodifiable_list);
final_list.add(5);
final_list.set(0, 100);
System.out.println("변경 후: " + final_list);
System.out.println(unmodifiable_list);
// final_list = new ArrayList<Integer>(); // 이 부분의 주석을 풀면 컴파일이 되지 않는다.
}
}
실행 결과는 다음과 같다.
final 키워드를 붙여서 선언하더라도 요소를 추가, 변경 (여기서 하진 않았지만 삭제도 가능하다.)할 수 있다.
가장 아래 줄 주석처리된 부분을 보면, 저 부분의 주석을 풀게 되면 컴파일 에러가 발생한다. final로 선언했기 때문에 다른 객체로 참조를 바꿔주는 것(즉, 변수 안에 저장된 주소 값을 바꿔버리는 것)은 불가능하기 때문이다.
Collections.unmodifiableList()라는 메소드를 통해서 불변 리스트(불변 컬렉션)을 만들 수 있다.
Java 표준 라이브러리의 일부로 제공되며, 기존 리스트를 불변 뷰로 감싸서 반환하는 메소드이다.
import java.util.*;
import java.io.*;
class Main {
static final List<Integer> unmodifiable_list = Collections.unmodifiableList(Arrays.asList(1, 2, 3));
public static void main(String[] args) {
System.out.println("변경 전: " + unmodifiable_list);
unmodifiable_list.add(99);
unmodifiable_list.set(2, 1000);
System.out.println("변경 후: " + unmodifiable_list);
}
}
Arrays.asList(요소들)로 만들 리스트를 unmodifiableList()로 감싸준 코드이다.
실행 결과
UnsupportedOperationException이 발생하며 변경이 되지 않는 것을 확인할 수 있다.
unmodifiableList()로 감싸서 반환된 객체는 원본 객체를 필드로 가지는 컬렉션이 된다. 그리고 read하는 메소드들은 원본 객체의 메소드로 실행된다.
즉, 위 예제에서 'unmodifiable_list'에 대해 get() 메소드를 호출하면 원본 객체에 대해서 get이 호출되고 그 결과를 반환되는 것이다.
하지만 add 등과 같은 컬렉션의 요소를 변경할 수 있는 메소드들은 막아놓았다.
원본 객체에 대한 메소드를 그대로 사용하는 '불변 뷰'를 제공하는 메소드이기 때문에, 기존 객체를 변경하면 그 불변 뷰의 내용도 바뀐다. 아까 final만으로는 불변 컬렉션을 만들 수 없다는 것을 보여주었던 예시 코드를 다시 보며 무슨 말인지 이해해보자.
import java.util.*;
import java.io.*;
class Main {
static final List<Integer> final_list = new ArrayList<>(Arrays.asList(1, 2, 3));
static final List<Integer> unmodifiable_list = Collections.unmodifiableList(final_list);
public static void main(String[] args) {
System.out.println("변경 전: " + final_list);
System.out.println(unmodifiable_list);
final_list.add(5);
final_list.set(0, 100);
System.out.println("변경 후: " + final_list);
System.out.println(unmodifiable_list);
// final_list = new ArrayList<Integer>(); // 이 부분의 주석을 풀면 컴파일이 되지 않는다.
}
}
실행 결과:
결과를 보면 원본 객체의 add(), set() 메소드 등을 호출해서 요소들을 변경하면 그 원본의 뷰에 해당하는 unmodifiable_list의 요소도 바뀐 것을 확인할 수 있다. unmodifiable_list에서 get()과 같은 read 메소드를 호출하면 그것은 결국 원본 객체에서 get()을 호출한 결과이기 때문이다.
따라서 unmodifiableList()류의 메소드는 다음의 경우에 사용하면 좋다.
unmodifiableList() 말고도 unmodifiableMap(), unmodifiableSet(), unmodifiableSortedSet() 등등이 존재하니 컬렉션의 종류에 따라 불변 뷰를 생성할 수 있다.
Java 9부터는 List.of()를 이용해서 immutable list를 만들 수 있게 되었다.
import java.util.*;
import java.io.*;
class Main {
static final List<Integer> immutable_list = List.of(1, 2, 3);
public static void main(String[] args) {
System.out.println("변경 전: " + immutable_list);
immutable_list.add(99);
System.out.println("add 후: " + immutable_list);
immutable_list.set(2, 1000);
System.out.println("set 후: " + immutable_list);
immutable_list.remove(1);
System.out.println("remove 후: " + immutable_list);
System.out.println("변경 후: " + immutable_list);
}
}
List.of(1, 2, 3)의 리턴 값을 저장해놓은 immutable_list에 add(), set(), remove() 등의 메소드를 호출하면 UnsupportedOperationException이 발생하는 것을 확인할 수 있다.
(위 예제를 보면, add()에서 Exception이 발생하는 것이지만, add(), set(), remove()의 실행 순서를 바꾸어 보면 저 세 메소드 모두 Exception을 발생시키면서 변경이 되지 않는다.)
unmodifiableList()와 다른 점이자 중요한 점은 뷰가 아닌 객체의 복사본을 따로 생성한다는 점이다.
List.of() 뿐만 아니라 Set.of()도 있다. (이따가 보겠지만 Map.of()도 있다.)
import java.util.Set;
class Main {
public static void main(String[] args) {
Set<Integer> s = Set.of(1, 2);
System.out.println(s);
s.add(9);
}
}
위 예제를 실행한 것이다. Set.of()를 통해서 만든 셋은 변경할 수 없다. add(Object o), remove(Object o)등 Set의 내용을 변경하려는 메소드를 호출하면 UnsupportedOperationException이 발생한다.
참고로 다음과 같이 Set.of()의 매개변수로 중복된 값을 주면 IllegalArgumentException이 발생한다.
Set<Integer> s = Set.of(1, 2, 3, 1); // 1이 중복인 상태
Map<String, Integer> map = Map.of("A", 10, "B", 11, "C", 12);
불변 맵은 위와 같이 Map.of로 만들 수 있다.
여기서 Map.of의 매개변수는 '키, 밸류, 키, 밸류, ...'이와 같은 순서로 받으므로, 매개변수의 개수는 항상 짝수여야 한다.
지금까지 List.of(), Map.of(), Set.of()에 대해서 알아보았는데, 이 기능들은 java 9에서 추가 되었다.
그렇다면 Java 8 이전 버전에서는 불변 리스트를 어떻게 만드는 것일까?
아래와 같이 스트림을 이용하여 만들 수 있다.
// 자바 8 이전에서
List<Integer> list = Collections.unmodifiableList(Stream.of(1, 2, 3).collect(toList()));
Set<String> set = Collections.unmodifiableSet(Stream.of("a", "b", "c").collect(toSet()));
이 메소드는 약간 애매하다.
Arrays.asList()로 생성한 리스트는 add, remove는 불가능하나, set() 메소드로 기존 원소의 값을 변경하는 것은 가능하기 때문이다.
다음 코드를 실행해보면
import java.util.*;
class Main {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3);
System.out.println(list);
list.set(1, 99);
System.out.println(list);
list.remove(99);
list.add(1);
}
}
set(1,99) (인덱스 1번에 있는 값을 99로 바꾸라는 의미) 메소드는 문제 없이 잘 실행되고 값까지 바뀐 것을 확인할 수 있다.
하지만 remove, add에 대해서는 UnsupportedOperationException이 발생한다. 물론 저 예제에서는 remove에 대해 발생한 것이겠지만, 메소드 실행 순서를 바꿔서 add가 먼저 실행되게 하더라도 동일한 예외가 뜨게 된다.
참고: Arrays.asList()와 List.of()의 차이
- Arrays.asList()는 add, remove 등은 안되지만(exception 발생), set을 통해서 특정 인덱스에 있는 원소를 바꾸는 것은 가능하다. 하지만 List.of()는 add, remove, set 모두 불가능하다.
- Arrays.asList(null)은 허용되지만, List.of(null)은 허용되지 않는다.
google에서 제공한 guava라는 라이브러리에는 ImmutableList()과 같은 클래스가 있다고 한다.
이는 나중에 다뤄보도록 하고, 외부 라이브러리 없이 불변 컬렉션을 만드는 방법은 이정도가 있을 것 같다.
https://unluckyjung.github.io/java/2021/02/20/Java-Unmodifiable/