본문으로 건너뛰기
개발 머꼬
개발 노트Java
hohyeon.dev15

stream().toList()로 만든 리스트에 add를 했더니 UnsupportedOperationException이 난 이유

  • #Collections
  • #Engineering Note
  • #Java

문제 발생

collect(Collectors.toList())를 더 짧은 toList()로 바꿨더니 컴파일은 통과하고 테스트에서 터졌습니다.

List<String> names = users.stream().map(User::name).toList();
names.add("추가");   // UnsupportedOperationException

원인 분석

Stream.toList()는 Java 16에 추가됐고, 자바독이 명시합니다 — 반환되는 List는 수정할 수 없으며(unmodifiable), 어떤 변경 메서드를 호출해도 항상 UnsupportedOperationException을 던집니다.

collect(Collectors.toList())는 다릅니다. 이쪽은 반환 타입이나 수정 가능 여부를 보장하지 않습니다. 실제 구현이 ArrayList를 돌려주기 때문에 관행적으로 수정할 수 있었을 뿐, 명세가 약속한 적은 없습니다. 두 메서드는 이름이 비슷하지만 계약이 다릅니다.

수정 가능 여부비고
stream().toList()불가(명시적 보장)Java 16+
collect(Collectors.toList())보장 없음관행적으로 ArrayList
collect(Collectors.toUnmodifiableList())불가(명시적 보장)Java 10+

자바독은 반환되는 인스턴스의 구현 타입과 직렬화 가능 여부도 보장하지 않고, 값 기반(value-based)일 수 있으므로 동일성(identity)에 대해 아무 가정도 하지 말라고 덧붙입니다. 즉 == 비교나 리스트 자체를 락 객체로 쓰는 코드도 안전하지 않습니다.

해결 방안

  1. 읽기 전용이면 toList()가 기본값입니다. 대부분의 스트림 결과는 만든 뒤 읽기만 하므로, 불변인 편이 오히려 안전합니다. 실수로 바뀌는 일이 컴파일이 아니라 즉시 예외로 드러납니다.
  2. 수정해야 한다면 명시적으로 가변 컬렉션에 담습니다. 의도가 코드에 남습니다.
List<String> names = new ArrayList<>(users.stream().map(User::name).toList());
names.add("추가");

또는 처음부터 원하는 구현을 지정합니다.

List<String> names = users.stream()
    .map(User::name)
    .collect(Collectors.toCollection(ArrayList::new));
  1. 정렬하려고 수정하지 않습니다. list.sort(...)는 변경 메서드입니다. 스트림 단계에서 정렬하면 불변 리스트를 그대로 쓸 수 있습니다.
List<String> sorted = users.stream().map(User::name).sorted().toList();
  1. API 경계에서는 불변을 그대로 넘깁니다. 반환한 리스트를 호출자가 바꾸면 내부 상태가 새는 문제가 생기는데, toList()는 그 방어 복사를 공짜로 해 줍니다.
  2. Arrays.asList와도 다릅니다. 이쪽은 크기만 고정이고 set()은 되는 어중간한 상태라 더 헷갈립니다. 셋을 섞어 쓰지 말고 의도에 따라 하나를 고릅니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.