스트림 패러다임의 핵심은 계산을 일련의 변환(transformation)으로 재구성하는 부분이다.
각 변환 단계는 순수 함수
여야 한다.
즉, 다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않는다.
+) 순수 함수란, 오직 입력만이 결과에 영향을 주는 함수를 말한다.
이렇게 하려면 스트림 연산에 건네는 함수 객체는 모두 부작용(side effect)이 없어야 한다.
forEach 연산은 종단 연산 중 기능이 가장 적고 가장 '덜' 스트림다운 연산이다.
이 연산은 대놓고 반복적이라서 병렬화할 수도 없다.
forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지 말자.
물론 가끔은 스트림 걔산 결과를 기족 컬렉션에 추가하는 등의 다른 용도로도 쓸 수 있다.
다음 코드는 스크림 코드를 가장한 반복적 코드로써, 스트림 API를 사용했지만 이점을 살리지 못 하였다.
이 코드는 같은 기능의 반복적 코드보다 길고, 어렵고, 유지보수에도 좋지 않다.
// 코드 46-1 스트림 패러다임을 이해하지 못한 채 API만 사용했다
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
위의 예제를 올바르게 작성하면 다음과 같다.
// 코드 46-2 스트림을 제대로 활용해 빈도표를 초기화한다.
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words
.collect(groupingBy(String::toLowerCase, counting()));
}
스트림을 사용하려면 꼭 배워야하는 개념이 수집기(collector)다.
java.util.stream.Collectors
클래스는 메서드를 무려 39개나 가지고 있다. (자바 8 기준, 10 기준으로는 43개)
🔖 자세한 내용은 공식 문서를 참고하자.
https://docs.oracle.com/javase/10/docs/api/java/util/stream/Collectors.html
이제 메서드들을 하나씩 알아보자.
각각 스트림의 원소들을 리스트, 집합, 프로그래머가 지정한 컬렉션 타입에 담아 반환한다.
toList()
toSet()
toCollection(collectionFactory)
스트림 원소들을 맵에 넣어 반환한다.
toMap(keyMapper, valueMapper)
toMap(keyMapper, valueMapper, mergeFunction)
toMap(keyMapper, valueMapper, mergeFunction, mapFactory)
toConcurrentMap(keyMapper, valueMapper)
toConcurrentMap(keyMapper, valueMapper, mergeFunction)
toConcurrentMap(keyMapper, valueMapper, mergeFunction, mapFactory)
classifier(분류 함수)를 받아 카테고리별로 모아 놓은 맵을 담은 수집기를 반환한다.
카테고리가 해당 원소의 맵 키로 쓰인다.
groupingBy(classifier)
groupingBy(classifier, downstream)
groupingBy(lassifier, mapFactory, downstream)
groupingByConcurrent(classifier)
groupingByConcurrent(classifier, downstream)
groupingByConcurrent(classifier, mapFactory, downstream)
분류 함수 자리에 predicate를 받고 키가 Boolean인 맵을 반환한다.
partitioningBy(predicate)
partitioningBy(predicate, downstream)
counting()
counting 메서드가 반환하는 수집기는 다운스트림 수집기 전용이다.
💡 groupingBy의 다운 스트림으로 사용한 예제
Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));
Stream의 count 메서드를 직접 사용하여 같은 기능을 수행할 수 있으니 collect(counting()) 형태로 사용할 일은 전혀 없다.
counting()
과 마찬가지로 직접 사용할 일은 없다.
summingDouble(mapper)
summingInt(mapper)
summingLong(mapper)
averagingDouble(mapper)
averagingInt(mapper)
averagingLong(mapper)
summarizingDouble(mapper)
summarizingInt(mapper)
summarizingLong(mapper)
counting()
과 마찬가지로 직접 사용할 일은 없다.
대부분의 프로그래머는 이들의 존재를 모르고 있어도 상관없다.
reducing(op)
reducing(identity, op)
reducing(identity, mapper, op)
filtering(predicate, downstream)
mapping(mapper, downstream)
flatMapping(mapper, downstream)
collectingAndThen(downstream, finisher)
인수로 받은 비교자를 이용해 스트림에서 값이 가장 작은/큰 원소를 찾아 반환한다.
Stream 인터페이스의 min과 max 메서드를 일반화한 것이다.
minBy(comparator)
maxBy(comparator)
joining 메서드는 CharSequence 인스턴스의 스트림에만 적용할 수 있으며, 원소들을 연결한다.
joining()
joining(delimiter)
joining(delimiter, prefix, suffix)
책에는 없지만, 자바 10부터 다음의 메서드가 추가되었다.
toUnmodifiableList()
toUnmodifiableMap(keyMapper, valueMapper)
toUnmodifiableMap(keyMapper, valueMapper, mergeFunction)
toUnmodifiableSet()
📌 핵심 정리
스트림 파이프라인 프로그래밍의 핵심은 부작용 없는 함수 객체에 있다.
스트림 뿐 아니라 스트림 관련 객체에 건네지는 모든 함수 객체가 부작용이 없어야 한다.
종단 연산 중forEach
는 스트림이 수행한 계산 결과를 보고할 때만 이용해야 한다. 계산 자체에는 이용하지 말자.
스트림을 올바로 사용하려면 수집기(collector)를 잘 알아둬야 한다.
가장 중요한 수집기 팩터리는toList
,toSet
,toMap
,groupingBy
,joining
이다.