스트림이 제공하는 표현력, 속도, (상황에 따라) 병렬성을 얻으려면 API와 이 패러다임까지 함께 받아들여야 한다.
계산을 일련의 변환으로 재구성하는 부분이 핵심
각 변환 단계는 가능한 한 이전 단계의 결과를 받아 처리하는 순수 함수여야 한다.
즉, 스트림 연산에 건네는 함수 객체는 모두 부작용이 없어야 한다.
순수 함수 : 오직 입력만이 결과에 영향을 주는 함수 다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않는다.
🍑 본론
스트림 패러다임의 사용
스트림 패러다임을 이해하지 못한 채 API만 사용한 경우
텍스트 파일에서 단어별 수를 세는 메서드
// Key : 단어, Value : 해당 단어가 나타난 횟수(빈도)
Map<String, Long> freq = new HashMap<>();
// 단어를 읽어 스트림 생성. tokens() : 단어 단위로 토큰을 생성하는 스트림 반환.
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> {
// freq.merge : 주어진 Key(단어)에 대해 Value(빈도수)를 맵에 추가 및 업데이트
// merge(맵에 추가할 키, 키가 맵에 존재하지 않을 때 사용할 초기값, 키가 이미 맵에 존재할 경우 기존 값과 1L의 합)
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
올바른 결과를 도출하지만 스트림 코드라고 할 수 없다.
스트림을 가장한 반복문
forEach는 스트림이 수행한 연산 결과를 보여주는 일 이상을 한기에 나쁜 코드(람다가 상태를 수정함)
스트림을 제대로 사용한 경우
텍스트 파일에서 단어별 수를 세는 메서드
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
// Collectors 메서드 사용
// 스트림의 각 요소(단어)를 소문자로 변환 후, 그것들을 그룹화하고 각 그룹의 요소 수(단어의 빈도) 계산
freq = words
.collect(groupingBy(String::toLowerCase, counting()));
}
forEach 연산은 대놓고 반복적이라 병렬화할 수 없다.
forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 사용하지 말자.
Scanner의 스트림 메서드인 tokens를 사용해 스트림을 얻었다. tokens는 자바9부터 지원하므로, 그 이전 버전을 쓰는 사람은 어댑터를 이용하여(Iterator를 구현한) Scanner를 스트림으로 변환할 수 있다. 코드47-3에서 사용하는 streamof(Iterable)가좋은예다.
collector
자주 사용하는 작업은 java.util.stream.Collectors 클래스에서 제공
39개의 메서드
복잡한 세부 내용을 몰라도 해당 API 장점 대부분 활용 가능
축소(reduction) 전략을 캡슐화한 블랙박스 객체라고 생각하자
축소 : 스트림의 원소들을 객체 하나에 취합
일반적으로 컬렉션 객체를 생성하며 collector라고 한다
종단 연산으로 forEach보단 Collectors 권장
toList() : 리스트 수집
toSet() : 집합 수집
toCollection(collectionFactory) : 지정한 컬렉션 타입 수집
1. toList
freq 맵을 사용해 가장 빈도수 높은 단어 10개 선택 및 리스트로 수집
List<String> topTen = freq.keySet().stream()
// sorted : 스트림의 요소 정렬
// comparing(freq::get) : freq 맵에서 각 Key(단어)에 대응하는 Value(빈도수) 기준으로 정렬한 Comparator 생성
// reversed() : 빈도수 높은 단어부터 낮은 단어 순 정렬
.sorted(comparing(freq::get).reversed())
.limit(10)
.collect(toList());
Collectors의 나머지 36개 메서드들 중 대부분은 스트림을 맵으로 취합하는 기능으로 진짜 컬렉션에 취합하는 것보다 훨씬 복잡하다.
스트림의 각 원소는 키 하나와 값 하나에 연관되어 있다. 또한 다수의 스트림 원소가 같은 키에 연관될 수 있다.
이제부터 수십 개의 메서드를 요약해 설명할 것이다. 따라서 본문의 글만으로는 내용이 헷갈리고 머리에 잘 남지 않을 수 있다. 흐름을 좇기 어렵다면 java.util.stream.Collectors의 API문서(http://bit.ly/2MvTOAR)를 펼쳐놓고 하나씩 짚어가며 읽어보길 추천한다.
2. toMap
인수가 2개인 toMap
toMap(keyMapper, valueMapper)
toMap(스트림 원소를 키에 매핑하는 함수, 값에 매핑하는 함수)
문자열을 열거 타입 상수에 매핑
private static final Map<String, item34.Operation> stringToEnum =
// Object::toString : enum 상수를 문자열로 변환
// e -> e : 값 매핑 함수로, 스트림의 각 요소를 그대로 값으로 사용
Stream.of(values()).collect(
toMap(Object::toString, e -> e));
이 형태는 스트림의 각 원소가 고유한 키에 매핑되어 있을 때 적합
스트림 원소 다수가 같은 키를 사용한다면 파이프라인이 IllegalStateException을 던지며 종료
더 복잡한 형태의 toMap이나 groupingBy는 이런 충돌을 다루는 다양한 전략 제공
toMap에 키 매퍼와 값 매퍼는 물론 병합(merge) 함수까지 제공
병합 함수의 형태는 BinaryOperator<U>, U는 맵의 값 타입
ex) 병합 함수가 곱셈이라면 키가 같은 모든 값을 곱한 결과
인수가 3개인 toMap
어떤 Key와 그에 연관된 Value들 중 하나를 골라 연관 짓는 맵을 만들 때 유용
세 번째 인수 : 병합함수(merge function)
두 Value가 같은 Key에 맵핑될 경우, 어떻게 병합할지 결정
각 키와 해당 키의 특정 원소를 연관짓는 맵 생성
다양한 음악가와 그 음악가의 베스트 앨범을 연관 짓기
Map<Artist, Album> topHits = albums.collect(
// Album::artist : 앨범의 아티스트가 Key
// a -> a : 앨범이 Value
// maxBy(comparing(Album::sales)) : 두 값이 같은 Key에 맵핑될 경우, 두 앨범 중 판매량이 더 높은 앨범을 선택
toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));
마지막에 쓴 값을 취하는 맵 생성
맵핑 함수가 Key 하나에 연결해준 Value들이 모두 같을 때, 혹은 다르더라도 모두 허용되는 값일 때 필요
groupingBy가 반환하는 Collector가 리스트 외의 값을 갖는 맵을 생성하게 하려면 분류 함수와 함께 다운스트림 컬렉터도 명시해야한다.
다운스트림 컬렉터 : 스트림의 요소를 그룹화(grouping), 분할(partitioning), 또는 다른 복잡한 집계(aggregation) 연산을 수행할 때, 하나의 컬렉터가 결과를 수집하는 과정에서 추가적으로 다른 컬렉터를 사용할 때 내부적으로 사용하는 컬렉터
toSet() : groupingBy 원소들의 리스트가 아닌 Set을 값으로 갖는 맵 생성
toCollection(collectionFactory) : 리스트나 집합 대신 컬렉션을 값으로 갖는 맵 생성
counting() : 각 카테고리(Key)를 해당 카테고리에 속하는 원소의 개수(Value)와 맵핑한 맵 생성
Map<String, Long> freq = words
.collect(groupingBy(String::toLowerCase, counting()));
인수가 3개인 groupingBy
두 번째 인수 : 맵 팩터리
세 번째 인수 : 다운스트림 컬렉터
점층적 인수 목록 패턴에 어긋난다.(맵 팩터리 매개변수가 다운스트림 매개변수보다 앞에 위치)
값이 TreeSet인 TreeMap을 반환하는 collector 생성 가능
// gpt
public class TreeMapTreeSetCollector {
public static void main(String[] args) {
List<String> items = List.of("banana", "apple", "cherry", "apple", "banana", "cherry", "orange");
// 분류 함수로 첫 글자를 사용하고, 결과를 TreeMap에 저장하며, 각 그룹의 값들을 TreeSet으로 수집하는 Collector 생성
Map<Character, TreeSet<String>> groupedItems = items.stream()
.collect(Collectors.groupingBy(
item -> item.charAt(0), // 분류 함수: 첫 글자로 그룹화
TreeMap::new, // 결과를 TreeMap으로 저장
Collectors.toCollection(TreeSet::new) // 각 그룹의 값들을 TreeSet으로 수집
));
System.out.println(groupedItems);
}
}
groupingByConcurrent
메서드의 동시 수행 버전으로 ConcurrentHashMap 인스턴스 생성
병렬 스트림에서 thread safe하게 요소를 그룹화할 때 사용
4. 기타 메서드
partitioningBy
많이 사용 X
groupingBy의 사촌격
분류 함수 자리에 프레디키트를 받고 Key가 Boolean인 맵 반환
Predicate : 자바에서 조건을 표현하는데 사용되는 함수형 인터페이스
사용할 일 없는 메서드
counting 메서드가 반환하는 collector는 다운스트림 컬렉터 전용
Stream의 count 메서드를 직접 사용하여 같은 기능을 수행할 수 있으니 collect(counting()) 형태로 사용할 일은 전혀 없다.
Collections에 이런 속성의 메서드가 16개 더 있다.
그 중 9개는 summing, averaging, summarizing으로 시작하며, 각각 int, long, double 스트림용으로 하나씩 존재
설계 관점에서 스트림 기능의 일부를 복제하여 다운스트림 컬렉터를 작은 스트림처럼 동작하게 하는 메서드로 몰라도 된다.
Chapter : 7. 람다와 스트림
Item : 46. 스트림에서는 부작용 없는 함수를 사용하라
Assignee : heon118
🍑 서론
스트림 패러다임
🍑 본론
스트림 패러다임의 사용
스트림 패러다임을 이해하지 못한 채 API만 사용한 경우
스트림을 제대로 사용한 경우
collector
1. toList
freq 맵을 사용해 가장 빈도수 높은 단어 10개 선택 및 리스트로 수집
Collectors의 나머지 36개 메서드들 중 대부분은 스트림을 맵으로 취합하는 기능으로 진짜 컬렉션에 취합하는 것보다 훨씬 복잡하다.
스트림의 각 원소는 키 하나와 값 하나에 연관되어 있다. 또한 다수의 스트림 원소가 같은 키에 연관될 수 있다.
2. toMap
인수가 2개인 toMap
toMap(keyMapper, valueMapper)
toMap(스트림 원소를 키에 매핑하는 함수, 값에 매핑하는 함수)
문자열을 열거 타입 상수에 매핑
이 형태는 스트림의 각 원소가 고유한 키에 매핑되어 있을 때 적합
스트림 원소 다수가 같은 키를 사용한다면 파이프라인이 IllegalStateException을 던지며 종료
더 복잡한 형태의 toMap이나 groupingBy는 이런 충돌을 다루는 다양한 전략 제공
toMap에 키 매퍼와 값 매퍼는 물론 병합(merge) 함수까지 제공
병합 함수의 형태는
BinaryOperator<U>
, U는 맵의 값 타입ex) 병합 함수가 곱셈이라면 키가 같은 모든 값을 곱한 결과
인수가 3개인 toMap
각 키와 해당 키의 특정 원소를 연관짓는 맵 생성
마지막에 쓴 값을 취하는 맵 생성
인수가 4개인 toMap
toConcurrentMap
3. groupingBy
인수가 1개인 groupingBy
인수가 2개인 groupingBy
두 번째 인수 : 다운스트림 컬렉터
groupingBy가 반환하는 Collector가 리스트 외의 값을 갖는 맵을 생성하게 하려면 분류 함수와 함께 다운스트림 컬렉터도 명시해야한다.
toSet() : groupingBy 원소들의 리스트가 아닌 Set을 값으로 갖는 맵 생성
toCollection(collectionFactory) : 리스트나 집합 대신 컬렉션을 값으로 갖는 맵 생성
counting() : 각 카테고리(Key)를 해당 카테고리에 속하는 원소의 개수(Value)와 맵핑한 맵 생성
인수가 3개인 groupingBy
값이 TreeSet인 TreeMap을 반환하는 collector 생성 가능
groupingByConcurrent
4. 기타 메서드
partitioningBy
사용할 일 없는 메서드
counting 메서드가 반환하는 collector는 다운스트림 컬렉터 전용
Stream의 count 메서드를 직접 사용하여 같은 기능을 수행할 수 있으니 collect(counting()) 형태로 사용할 일은 전혀 없다.
Collections에 이런 속성의 메서드가 16개 더 있다.
그 중 9개는 summing, averaging, summarizing으로 시작하며, 각각 int, long, double 스트림용으로 하나씩 존재
설계 관점에서 스트림 기능의 일부를 복제하여 다운스트림 컬렉터를 작은 스트림처럼 동작하게 하는 메서드로 몰라도 된다.
Collectors에 정의되어 있지만 '수집'과는 관련 없는 메서드
minBy / maxBy
joining
🍑 결론
스트림 파이프라인 프로그래밍의 핵심은 부작용 없는 함수 객체에 있다.
스트림뿐 아니라 스트림 관련 객체에 건네지는 모든 함수 객체가 부작용이 없어야 한다.
Referenced by