티스토리 뷰
Stream.toList로 Stream.collect(toList())를 대체해도 되는 걸까?
KimDoubleB 2022. 5. 7. 23:39최근 JDK 17 release 되었고, JDK 11 다음의 LTS 버전으로서 오랫동안 지원이 되는 만큼 업무/개인프로젝트 등에서 적극적으로 사용하고 있다.
Stream.toList()
JDK17 기반에서 Stream을 사용해 List로 변환(collect)하다보면 IntelliJ에서 자주 보는 메세지가 있다.
위의 경우인데, collect(toList())
대신 Stream.toList()
를 사용할 수 있다는 정보성 메시지이다.
그래서 나는 보통 JDK 17로 개발을 할 때, 아래와 같이 사용했다.
// Before jdk 17
var peopleName = people.stream()
.map(Person::name)
.collect(Collectors.toList());
// jdk 17
var peopleName = people.stream()
.map(Person::name)
.toList();
이게 가능해진 건 Stream.toList()
메서드가 생겼기 때문이다.
- 해당 메서드는 JDK 16에서 생겼다. (참조)
- 기존의
collect(Collectors.toList())
부분을toList()
메서드 하나로 줄여서 더 간결화했다.
Upgrade to JDK 17
그러면 기존의 JDK 16 이전의 프로젝트들에서 JDK 17로 버전 업그레이드를 하며 위에서 말한 list stream collect 부분을 toList
로 다 이전해도 되는걸까?
그건 상황에 따라 고려해봐야한다.
왜냐하면, collect(Collectors.toList())
와 toList()
가 완전히 똑같은 형태의 구현체로 반환되지 않기 때문이다.
collect(Collectors.toList())
는 ArrayList
를 반환한다.
- javadoc에는 아래와 같이 작성되어있다.
There are no guarantees on the type, mutability, serializability, or thread-safety of the List returned
toList()
는 Collectors.UnmodifiableList
또는 Collectors.UnmodifiableRandomAccessList
를 반환한다.
- UnmodifiableList 와 UnmodifiableRandomAccessList 의 차이는 현재 글의 주제와 좀 어긋나니 이후부터는 UnmodifiableList 라고만 이야기하겠다. 그리고 UnmodifiableRandomAccessList 도 사실 UnmodifiableList 를 상속해 구현한 구현체이다.
ArrayList
와 UnmodifiableList
. 둘의 구현체 차이가 보이는가?
- 이름에서 나타내는 것처럼 수정이 가능한 구현체와 수정이 불가능한 구현체라는 것이다.
그럼 이 구현체의 성질이 JDK 17로의 업그레이드 시에 어떤 영향을 미칠까?
예를 살펴보자. 아래와 같은 프로젝트 코드가 있다고하자.
public void someMethod() {
var peopleName = people.stream()
.map(Person::name)
.collect(Collectors.toList());
someBusinessLogic(peopleName);
...
}
private void someBusinessLogic(List<String> peopleName) {
peopleName.add("Name that should always be added");
}
특정 리스트를 stream을 통해 만들고, 마지막에 무조건 어떤 요소를 추가하는 로직이 있는 상황이다.
- 예제 코드이므로 억지스러워도 넘어가자.
이 예제를 JDK 17로 넘어가며 아래처럼 수정하게 된다면, UnsupportedOperationException
예외가 발생할 것이다.
public void someMethod() {
var peopleName = people.stream()
.map(Person::name)
.toList(); // UnmodifiableList
someBusinessLogic(peopleName);
...
}
private void someBusinessLogic(List<String> peopleName) {
peopleName.add("Name that should always be added"); // UnsupportedOperationException
}
위에서 이야기했듯 Stream.toList()
는 UnmodifiableList
를 반환하기에 List 자체에 대한 변경이 불가능하기 때문이다.
UnmodifiableList
구현체의 코드는 다음과 같다.
static class UnmodifiableList<E> extends UnmodifiableCollection<E>
implements List<E> {
...
public E get(int index) {return list.get(index);}
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
public int indexOf(Object o) {return list.indexOf(o);}
public int lastIndexOf(Object o) {return list.lastIndexOf(o);}
public boolean addAll(int index, Collection<? extends E> c) {
throw new UnsupportedOperationException();
}
@Override
public void replaceAll(UnaryOperator<E> operator) {
throw new UnsupportedOperationException();
}
@Override
public void sort(Comparator<? super E> c) {
throw new UnsupportedOperationException();
}
...
}
보는 것처럼 수정에 관련된 메서드들은 UnsupportedOperationException
예외를 던진다.
UnsupportedOperationException
예외는 RuntimeException
의 구현체로서 런타임 시점에 알 수 있기에 코드를 실행하기 이전에는 알지 못한다. 그렇기에 IDE 상에서도 위 예제 코드에서 오류를 예측하지 못한다.
그럼 toUnmodifiableList
는 대체가 가능한걸까?
collect(Collectors.toList())
는 이후에 List 수정로직이 존재한다면 Stream.toList()
로 대체하지 못한다.
그러면 collect(Collectors.toUnmodifiableList())
코드의 경우 가능한걸까? 아래처럼 말이다.
// Before jdk 17
var peopleName = people.stream()
.map(Person::name)
.collect(Collectors.toUnmodifiableList());
// jdk 17
var peopleName = people.stream()
.map(Person::name)
.toList();
가능하다. 하지만 둘의 차이가 있긴 있다. (TMI)
collect(Collectors.toUnmodifiableList())
의 경우 input list에 null
요소를 허용하지 않고, Stream.toList()
는 null
요소를 허용한다는 것이다.
예제를 보자.
void someMethod() {
var peopleName = people.stream()
.map(someMappingOtherValue())
.collect(Collectors.toUnmodifiableList());
...
}
privateFunction<Person,Person> someMappingOtherValue() {
returnperson-> {
if (person.age < 20) {
return null; // 여긴 무조건 들린다는 가정
}
returnperson;
};
}
위 예제의 경우, NPE가 발생한다. Collectors.toUnmodifiableList
메서드 내에서 ImmutableCollections.listFromTrustedArray
메서드를 호출해 새로운 리스트로 요소 데이터를 추가해 생성하게 되는데, 이때 요소들의 null 값 여부를 확인(Objects.requireNonNull
)하기 때문이다.
하지만 Stream.toList()
는 null
요소를 허용하므로 아래와 같이 수정해도 NPE가 발생하지 않는다.
void someMethod() {
var peopleName = people.stream()
.map(someMappingOtherValue())
.toList();
...
}
privateFunction<Person,Person> someMappingOtherValue() {
returnperson-> {
if (person.age < 20) {
return null; // 여긴 무조건 들린다는 가정
}
returnperson;
};
}
Summary
collect(toList())
- 수정이 허용된다.
- Null 값이 허용된다.
collect(toUnmodifiableList())
- 수정이 불가능하다.
- Null 값이 허용되지 않는다.
toList()
- 수정이 불가능하다.
- Null 값이 허용된다.
Java 버전을 올리면서 새로운 기능을 사용할 때 고려해야하는 점이 참 많은 것 같다. IDE에서는 “이런거 사용할 수 있어~ 바꿔봐~!”라고 계속 유혹하지만, 코드 로직에서 변경된 부분을 적용해도 발생하는 이슈가 없을 지 잘 찾아보고 변경하는 것이 중요하다는 것을 깨달을 수 있었다.
사실 근데 버전 업그레이드마다 이러한 부분을 세세히 알기 어렵다. 그러므로... “테스트 코드"를 작성하는 습관을 들이고 이를 통해 이러한 세세한 오류도 커버할 수 있도록 하는 것이 제일 베스트인 것 같다.
'Development > Java, Kotlin, Frameworks' 카테고리의 다른 글
R2DBC를 사용해보자 (1) - 왜 사용할까? (0) | 2022.08.15 |
---|---|
ThreadLocal 사용 시, remove를 잊지 맙시다 (0) | 2022.07.06 |
[IntelliJ IDEA] Java Stream Debugging (스트림 디버깅) (1) | 2022.05.04 |
Java Memory 이해 & OOME 오류에 관하여 (0) | 2022.04.27 |
Flame graph (Profiling) (0) | 2022.04.27 |
- Total
- Today
- Yesterday
- Istio
- WebFlux
- boj
- 클린 아키텍처
- tag
- docker
- 일상
- Log
- 알고리즘
- 쿠버네티스
- c++
- Intellij
- Algorithm
- Spring boot
- 로그
- jasync
- Clean Architecture
- HTTP
- java
- hexagonal architecture
- container
- gradle
- 비동기
- python
- Kubernetes
- 하루
- Spring
- k8s
- MySQL
- 백준
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |