스트림 패러다임의 핵심은 계산을 일련의 변환으로 재구성하는 부분이다.
이때 각 변환 단계는 가능한 한 이전 단계의 결과를 받아 처리하는 순수함수여야 한다.
순수 함수란 오직 입력만이 결과에 영향을 주는 함수를 말한다.
다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않는다.
이렇게 하려면 스트림 연산에 건네는 함수 객체는 모두 부작용이 없어야 한다.
Map<String, Long> freq = new HashMap<>();
try(Stream<String> words = new Scanner(file).tokens) {
words.forEach(
word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
}
);
}
위의 코드는 스트림 코드라고 할 수 없다.
스트림 코드를 가장한 반복적 코드다.
스트림 API의 이점을 살리지 못하여 같은 기능의 반복적 코드보다 길고, 읽기 어렵고, 유지보수에도 좋지 않다.
이 코드의 모든 작업이 종단 연산인 forEach에서 일어나는데, 이때 외부 상태(freq)를 수정하는 람다를 실행하면서 문제가 생긴다.
우선 위의 코드를 올바르게 고쳐보면 아래와 같다.
Map<String, Long> freq;
try(Stream<String> words = new Scanner(file).tokens()) {
frq = words.collect(groupingBy(String::toLowwerCase, counting()));
}
같은 일을 하지만 이번에는 스트림 API를 제대로 사용한 것이다.
forEach 연산은 종단 연산 중 기능이 가장 적고 가장 '덜' 스트림답다.
대놓고 반복적이라서 병렬화할 수도 없다.
forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지않는 것을 추천한다.
forEach를 대신하여 사용된 collector, 즉 수집기는 스트림을 사용하려면 꼭 배워야 하는 새로운 개념이다.
이는 우선 그저 축소(reduction) 전략을 캡슐화한 블랙박스 객체라고 생각하면 된다.
여기서 축소는 스트림의 원소들을 객체 하나에 취합한다는 뜻이다.