[Effective Java] 스트림에서는 부작용 없는 함수를 사용하라

들어가며

스트림 패러다임의 핵심은 계산을 일련의 변환으로 재구성하는 부분이다. 이때 각 변환 단계는 가능한 한 이전 단계의 결과를 받아 처리하는 순수 함수여야 한다. 순수 함수란 오직 입력만이 결과에 영향을 주는 함수를 말한다. 다음은 스트림 패러다임을 이해하지 못한 채 API만 사용한 예이다.

1
2
3
4
5
6
7
8
public static void main(String[] args) {
String str = "hello this is study of Effective java and i am newbie in java";
Stream<String> words = Arrays.stream(str.split(" "));
Map<String, Long> badFreq = new HashMap<>();
words.forEach(word -> {
badFreq.merge(word.toLowerCase(), 1L, Long::sum);
});
}

위 코드는 모든 작업이 종단 연산인 forEach에서 이뤄진다. 이때 외부 상태를 수정하는 람다를 실행하면서 문제가 생긴다. 종단 연산이 그저 스트림의 연산 결과를 보여주는 일 이상의 행위를 하고있는 것이다. forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고 계산하는데는 쓰지 말자. 다음 코드는 올바르게 작성된 코드이다.

1
2
3
4
5
6
public static void main(String[] args) {
String str = "hello this is study of Effective java and i am newbie in java";
Stream<String> words = Arrays.stream(str.split(" "));
Map<String, Long> freq;
freq = words.collect(groupingBy(String::toLowerCase, counting()));
}

collector

수집기는 스트림을 사용하기 위해서 반드시 알아야하는 개념이다. 다음 코드를 보자.

1
2
3
List<String> topTen = freq.keySet().stream().sorted(comparing(freq::get).reversed())
.limit(10)
.collect(Collectors.toList());

위 코드는 freq 빈도표에서 빈도수 상위 단어 10개를 추출하는 스트림 파이프라인이다. comparing(freq::get)으로 value를 가져와 역순으로 정렬한 뒤 상위 10개를 추출한다.

간단한 맵 수집기는 toMap(keyMapper, valueMapper)의 인수 두개를 받는 형태이다. 인수 3개를 받는 맵 수집기도 있다. toMap(keyMappter, valueMapper, userPrinciple)의 형태이다. 3번째 인수에 삽입하고자 하는 결과값을 넣을 수 있다. 예를 들어 최신 데이터로 교체하고 싶다면 (oldVal, newVal) -> newVal로, 최대값을 맵핑시키고 싶다면 maxBy(comparing(oldval))의 형태로 작성하면 된다.

groupingBy는 입력으로 분류 함수를 받고 출력으로는 원소들을 카테고리별로 모은 Map을 담은 수집기를 반환한다. 분류 함수는 입력받은 원소가 속하는 카테고리를 반환한다.

1
Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));

minBy 와 maxBy는 수집과는 관련 없는 메소드이다. 이 두 메소드는 인수로 받은 비교자를 통해 스트림에서 값이 작은 혹은 큰 원소를 반환한다.

Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/16/Languages/Effective%20JAVA/item46/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.