스트림은 그저 또 하나의 API가 아닌, 함수형 프로그래밍에 기초한 패러다임이다.
즉.. 스트림이 제공하는 표현력, 속도, 병렬성 등을 얻기 위해서는 '스트림 패러다임'을 받아들여야 한다.
스트림 패러다임의 핵심이란?
: 계산을 일련의 변환으로 재구성 하는 부분
- 각 변환 단계는 가능한 한 이전 단계의 결과를 받아 처리하는 순수 함수 (*다른 가변 상태를 참조하거나 스스로 상태를 변경하지 않고, 오직 입력만이 결과에 영향을 주는 함수)여야 한다.
- 이를 위해선 스트림 연산에 건네는 함수는 모두 부작용이 없어야 한다.
- forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지 말자.
스트림 패러다임을 이해하지 못한 채 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);
});
}
같은 기능을 하지만, 스트림을 제대로 사용한 예시도 보여주겠음!
forEach문은 스트림 계산 결과를 보고할 때만 사용하고, 계산할 때는 쓰지 말아라!
"스트림 패러다임을 제대로 이해한 코드"
Map<String , Long> freq = new HashMap<>();
try(Stream<String> words = new Scanner(file).tokens()){
freq = words.collect(groupingBy(String::toLowerCase, counting()));
}
수집기 (collector)
스트림을 사용하려면 'collector'라는 개념을 꼭 배워야 한다.
수집기가 생성하는 객체는 일반적으로 컬렉션이며, 그래서 'collector'라는 이름을 쓴다.
수집기를 사용하면 스트림의 원소를 손쉽게 컬렉션으로 모을 수 있다.
collector 종류
: toList(), toSet(), toCollection(collectionFactory) 가 있다. - 리스트, 집합, 프로그래머가 정한 타입을 반환하는 수집기
Ex)
1. toList() - 리스트를 반환
List<String > top10 = freq.keySet().stream()
.sorted(Comparator.comparing(freq::get).reversed())
.limit(10).
collect(Collectors.toList());
" freq:get을 통해 입력받은 단어를 찾아, 빈도를 반환한다.
가장 흔한 단어가 위로 오도록 comparing을 reverse로 정렬했다.
그 다음 10개를 뽑아 toList로 리스트에 담는다."
2. toMap() - 맵 수집기
toMap(keyMapper, valueMapper) 형태.
스트림 원소를 키에 매핑하는 함수와 값에 매핑하는 함수 두개를 인수로 받는다.
"1. 열거 타입 상수에 매핑"
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(
toMap(Object::toString, e -> e));
- 스트림 각 원소가 고유 키에 매핑되어 있는 형태에 적합. (다수가 같은 키를 사용한다면 IllegalStateException)
"2. 어떤 키와 그 키에 연관된 원소들 중 하나를 골라 연관짓는 맵 생성"
// 인수 3개를 받는 toMap
Map<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));
- maxBy를 비교자로 사용했다.
- 즉, 앨범 스트림을 맵으로 바꾸는데, 이 맵은 앨범의 음악가와 그 음악가의 베스트 앨범을 키 밸류로 하는 맵이다. 라는 말이다.
"3. 인수가 3개인 toMap에서 충돌이 나면 마지막 값을 취하는 수집기"
toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
3. groupingBy
입력으로 분류 함수를, 출력으로 원소들을 카테고리로 모아놓은 맵을 담은 수집기를 반환한다.
분류 함수는 입력받은 원소가 속하는 카테고리를 반환한다.
리스트로 매핑하는 맵을 생성하는 예시
words.collect(groupingBy(word -> alphaetize(word)));
groupingBy가 반환하는 수집기가 리스트 외의 값을 갖는 맵을 생성하게 하려면, 분류 함수와 함께 다운스트림 수집기도 명시해야 한다.
- 다운스트림의 역할은 해당 카테고리의 모든 원소를 담은 스트림으로부터 값을 생성하는 일이다.
- 간단히 toSet()을 넘겨 집합을 값으로 갖는 맵을 넘길 수 있다.
다운스트림 수집기로 counting()을 건내는 방법도 있다.
Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting())
- 각 카테고리를 원소를 담은 컬렉션이 아닌, 해당 카테고리에 속하는 원소 개수와 매핑한 맵을 얻는다.
4. joining
문자열 등의 CharSequence 인스턴스 스트림에만 적용할 수 있다.
매개변수가 없는 joining은 단순 원소들을 연결하는 수집기를 반환한다.
매개변수가 하나짜리인 joining은 구분문자를 매개변수로 받아 연결 부위에 이를 삽입한 csv형태의 문자열을 만든다.
매개변수가 3개짜리인 joining은 접두문자, 접미문자, 구분문자를 받아 컬렉션을 출력한 것과 같은 형태의 문자열을 만든다.
핵심 정리
스트림 파이프라인의 패러다임을 잘 숙지하자.
종단 연산에서 forEach는 스트림이 수행한 계산 결과를 보고할 때만 이용하자.
스트림 수집기를 잘 알아두자.
toList, toSet, toMap, groupingBy, joining은 스트림을 사용하는 개발자라면 숙지하는 것이 좋다.
https://docs.oracle.com/javase/10/docs/api/java/util/stream/Collectors.html
Collectors (Java SE 10 & JDK 10 )
Returns a Collector implementing a "group by" operation on input elements of type T, grouping elements according to a classification function, and returning the results in a Map. The classification function maps elements to some key type K. The collector p
docs.oracle.com
'JAVA > Effective Java' 카테고리의 다른 글
item 48. 스트림 병렬화는 주의해서 적용해라 (0) | 2022.07.02 |
---|---|
item 47. 반환 타입으로는 스트림보다 컬렉션이 낫다 (0) | 2022.07.02 |
item 45. 스트림은 주의해서 사용하라. (0) | 2022.06.18 |
item 44. 표준 함수형 인터페이스를 사용하라 (0) | 2022.06.18 |
item 43. 람다보다는 메서드 참조를 사용하라 (0) | 2022.06.17 |
댓글