stream().toList()로 만든 리스트에 add를 했더니 UnsupportedOperationException이 난 이유
문제 발생
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)에 대해 아무 가정도 하지 말라고 덧붙입니다. 즉 == 비교나 리스트 자체를 락 객체로 쓰는 코드도 안전하지 않습니다.
해결 방안
- 읽기 전용이면
toList()가 기본값입니다. 대부분의 스트림 결과는 만든 뒤 읽기만 하므로, 불변인 편이 오히려 안전합니다. 실수로 바뀌는 일이 컴파일이 아니라 즉시 예외로 드러납니다. - 수정해야 한다면 명시적으로 가변 컬렉션에 담습니다. 의도가 코드에 남습니다.
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));- 정렬하려고 수정하지 않습니다.
list.sort(...)는 변경 메서드입니다. 스트림 단계에서 정렬하면 불변 리스트를 그대로 쓸 수 있습니다.
List<String> sorted = users.stream().map(User::name).sorted().toList();- API 경계에서는 불변을 그대로 넘깁니다. 반환한 리스트를 호출자가 바꾸면 내부 상태가 새는 문제가 생기는데,
toList()는 그 방어 복사를 공짜로 해 줍니다. Arrays.asList와도 다릅니다. 이쪽은 크기만 고정이고set()은 되는 어중간한 상태라 더 헷갈립니다. 셋을 섞어 쓰지 말고 의도에 따라 하나를 고릅니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.