자바 언어·플랫폼 용어 사전
컬렉션List.of · unmodifiableList · Collections.unmodifiable · 방어적 복사본

불변 컬렉션

"못 바꾼다"에는 세 단계가 있다 — 뷰, 진짜 불변, 그리고 깊은 불변.

"못 바꾼다"는 말에는 세 단계가 있고, 섞어 쓰면 사고가 난다.

  • 수정 불가 뷰 — Collections.unmodifiableList(origin)

    • 원본이 바뀌면 뷰도 바뀐다 — 껍데기만 막았다
  • 진짜 불변 — List.of(a, b, c) / List.copyOf(origin)

    • 내부 배열을 자기가 들고 있어 원본과 무관하다
  • 깊은 불변 — 원소까지 불변이어야 성립한다

    • List.of(mutableUser) 의 user.setName() 은 여전히 통한다

①의 함정

List<String> origin = new ArrayList<>(List.of("a"));
List<String> view = Collections.unmodifiableList(origin);
origin.add("b");
view.size();   // 2 — 뷰는 원본을 그대로 비춘다

게터에서 이 뷰를 돌려주면 "불변을 반환했다"고 착각하지만, 원본 참조를 가진 쪽이 언제든 바꾼다. 진짜 보호하려면 List.copyOf로 복사본을 돌려준다.

List.of 의 추가 성질

· null 원소를 허용하지 않는다 (NullPointerException)
· 중복은 List 는 허용, Set.of·Map.of 는 IllegalArgumentException
· 반환 타입이 Arrays.asList 와 다르다
    Arrays.asList  고정 크기지만 set() 은 된다 (배열 뷰)
    List.of        set() 도 UnsupportedOperationException

왜 불변이 동시성에 유리한가

불변 객체는 안전 공개(safe publication) 만 되면 동기화 없이 여러 스레드가 공유해도 된다

생성자에서 대입된 final 필드는 생성 완료 시점이 보장되므로 다른 스레드가 '반쯤 만들어진' 상태를 보지 않는다

  • 방어적 복사·락을 모두 생략할 수 있다

스트림 수집 결과의 가변성

Collectors.toList()       // 가변인지 불변인지 명시되지 않음 (현재는 ArrayList)
Collectors.toUnmodifiableList()  // 명시적으로 수정 불가
Stream.toList()           // Java 16+, 수정 불가 (단 null 허용)

toList() 의 결과를 수정하는 코드는 '지금은' 동작한다 명세가 보장하지 않으므로 JDK 버전이 바뀌면 깨질 수 있다

  • 의도를 코드로 드러내는 쪽을 고른다

방어적 복사는 어디에 넣나

public final class Order {
    private final List<Item> items;

    public Order(List<Item> items) {
        this.items = List.copyOf(items);   // ① 들어올 때 복사
    }
    public List<Item> getItems() {
        return items;                      // ② 이미 불변이므로 그대로
    }
}
  • ① 을 빠뜨리면 호출자가 원본을 계속 들고 바꾼다

  • ② 에서 내부 리스트를 그대로 주면 호출자가 add() 로 뚫는다

  • record 도 마찬가지다 — 컴팩트 생성자에서 복사해야 진짜 불변이 된다

    • record Order(List<Item> items) {
      • Order { items = List.copyOf(items); }
    • }

면접 함정

  • "unmodifiableList는 불변이다" → 원본을 통해 바뀐다. 뷰일 뿐이다.
  • "List.of면 안에 든 객체도 안전하다" → 얕은 불변이다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 컬렉션·예외·리소스