스트림 패러다임의 핵심은 계산을 일련의 변환(transformation)으로 재구성하는 것이다. 여기서 각 변환은 가능한 이전 단계의 결과를 받아서 처리하는 순수 함수여야 한다. 이를 위해서는 스트림 연산에 건네는 함수 객체는 모두 side-effect가 없어야 한다.
아래의 예시를 보자.
1
2
3
4
5
6
7
|
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
})
}
|
cs |
이 코드는 스트림을 가장한 반복적 코드다. 위에서 명시했듯 스트림에서 사용하는 함수는 순수 함수여야 한다. 하지만 위 코드는 freq에 값을 추가하기에 스트림의 이점을 잘 살리지 못했다. 따라서 이런 코드는 단순 반복 코드보다 길고, 가독성이 떨어지고, 유지보수에 좋지 않다. 이 코드의 모든 연산은 종단 연산에서 발생하는 데, 이때 외부 상태를 수정하는 람다를 실행하면 문제가 발생한다.
forEach 연산은 대놓고 반복적이라 병렬화 할 수 없으며 종단 연산 중 가장 스트림 답지 않다. 따라서 스트림 연산 결과를 보고할 때만 사용하자.
forEach를 사용하는 대신 collector를 사용하라. 아래 예시는 위 코드를 collector를 사용해 바꾼 것이다.
1
2
3
4
5
6
|
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words
.collect(groupingBy(String::toLowerCase, counting()));
}
|
cs |
출처 - 이펙티브 자바