티스토리 뷰
스트림 패러다임
- 핵심은 계산을 일련의 변환으로 재구성하는 부분
- 각 변환 단계는 가능한 한 이전 단계의 결과를 받아 처리하는 순수 함수여야함
- 순수 함수 : 오직 입력만이 결과에 영향을 주는 함수(다른 가변 상태 참조/변경 x)
// 스트림 패러다임을 이해하지 못한 채 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); // 외부 상태 수정
});
}
위 코드는 스트림 API의 이점을 살리지 못한 스트림을 가장한 반복적 코드이다.
이 코드의 모든 작업은 종단연산인 forEach에서 일어나는데, 이 때 외부 상태(freq)를 수정하는 람다를 실행하면서 문제가 생김 → 그저 연산 결과를 보여주는 일 이상을 하는 안좋은 코드
// 스트림을 제대로 활용한 코드
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words.collect(groupingBy(String::toLowerCase, counting()));
}
위 코드가 스트림을 제대로 활용하여 더 명확하고 간결하다.
forEach 연산은 스트림 계산 결과를 보고할때만 사용하고 계산할때는 사용하지말자.
Collector 클래스
Collector를 사용하면 스트림의 원소를 손쉽게 컬렉션으로 모을 수 있다. (컬렉션 타입 반환)
종류 3가지 : toList(), toSet(), toCollection(collectionFactory)
//빈도표에서 가장 흔한 단어 10개를 뽑아내는 코드
Map<String, Long> freq = new HashMap<>();
List<String> topTen = freq.keySet().stream()
.sorted(Comparator.comparing(freq::get).reversed()) // 정렬 (비교자.역순)
.limit(10) // 10개 제한
.collect(Collectors.toList()); // 리스트로 반환
1) toMap(keyMapper, valueMapper)
스트림 원소를 키에 매핑하는 함수와 값에 매핑하는 함수를 인수로 받는다.
키가 고유해야만 가능 → 아니면 IllegalStateException 던짐
//toMap 수집기를 이용하여 문자열을 열거 타입 상수에 매핑
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(toMap(Object::toString, e -> e));
인수 3개를 받는 toMap은 어떤 키와 그 키와 연관된 특정 원소를 연관짓는 맵을 생성한다.
//다양한 음악가의 앨범을 담은 스트림을 가지고 음악가와 그 음악가의 베스트 앨범을 매핑
Map<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));
인수 3개를 받는 toMap으로 마지막에 쓴 값을 취하는 수집기를 만든다. (충돌이 날 때)
toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
2) groupingBy
입력으로 분류 함수(classifier)를 받고 출력으로 원소들을 카테고리별로 모은 맵을 반환
카테고리가 키, 해당 카테고리에 속하는 원소들을 모두 담은 리스트가 값이 된다.
words.collect(groupingBy(word -> alphabetize(word)));
만약 값으로 리스트 외의 값을 갖는 맵을 생성하게 하려면 분류 함수와 함께 다운스트림(downstream) 수집기도 명시해야함 (다운스트림 역할 : 해당 카테고리의 모든 원소를 담은 스트림으로부터 값을 생성하는 일)
groupingBy(Function<? super T, ? extends K> classifier,Collector<? super T, A, D>downstream)
다운스트림으로 toSet(), toCollection(collectionFactory), counting() 등 방법이 많다.
맵 팩터리도 지정할 수 있는 버전도 있다. 이 버전은 맵과 그 안에 담긴 컬렉션의 타입을 모두 지정할 수 있다.
groupingBy(Function<? super T,? extends K> classifier, Supplier<M> mapFactory, Collector<? super T,A,D> downstream)
3) partitioningBy
predicate 를 받고 키가 Boolean 인 맵을 반환한다.
다운스트림 수집기까지 받는 버전도 다중정의 되어있다.
4) maxBy, minBy
수집과는 관련이 없다. 인수로 받은 비교자를 이용해 스트림에서 값이 가장 큰값 / 작은 값을 반환
5) joining
- CharSequence 인스턴스의 스트림에만 적용 가능
- 매개변수가 없는 버전은 단순히 원소들을 연결하는 수집기를 반환
- 인수 1개 버전 : CharSequence 타입의 구분문자를 입력하면 CSV 형태의 문자열을 만들어준다.
- 인수 3개 버전 : 구분 문자에 더해 접두문자(prefix)와 접미문자(suffix)도 받음
결론
스트림뿐 아니라 스트림 관련 객체에 건네지는 모든 함수 객체가 부작용이 없어야한다.
스트림을 올바로 사용하기 위해서는 수집기를 알아둬야 한다.
중요한 수집기 팩터리는 toList, toSet, toMap, groupingBy, joining 이다.
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] 48. 스트림 병렬화는 주의해서 적용하라 (0) | 2022.04.01 |
---|---|
[Effective Java] 47. 반환 타입으로는 스트림보다 컬렉션이 낫다 (0) | 2022.04.01 |
[Effective Java] 45. 스트림은 주의해서 사용하라 (0) | 2022.04.01 |
[Effective Java] 44.표준 함수형 인터페이스를 사용하라 (0) | 2022.04.01 |
[Effective Java] 43.람다보다는 메서드 참조를 사용하라 (0) | 2022.04.01 |