chapter6 - 스트림으로 데이터 수집
- 람다가 없다면
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();
for (Transaction transaction : transactions) {
Currency currency = transaction.getCurrency();
List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency);
if (transactionsForCurrency == null) {
transactionsForCurrency = new ArrayList<>();
transactionsByCurrencies.put(currency, transactionsForCurrency);
}
transactionsForCurrency.add(transaction);
}
System.out.println(transactionsByCurrencies);
- java 8에서는
Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream().collect(groupingBy(Transaction::getCurrency));
System.out.println(transactionsByCurrencies);
6.1 컬렉터란 무엇인가?
6.1.1 고급 리듀싱 기능을 수행하는 컬렉터
- collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의할 수 있다는 점.
- Collector 인터페이스의 메서드를 어떻게 구현하느냐에 따라 스트림에 어떤 리듀싱 연산을 수행할지 결정된다.
- Collectors 유틸리티 클래스는 자주 사용하는 컬렉터 인스턴스를 손쉽게 생성할 수 있는 정적 팩토리 메서드를 제공한다.
List<Transaction> transactions = transactionStream.collect(Collectors.toList());
6.1.2 미리 정의된 컬렉터
- Collectors 제공 메서드
- 스트림 요소를 하나의 값으로 리듀스하고 요약
- 요소 그룹화
- 요소 분할
6.2 리듀싱과 요약
카운팅
import static java.util.stream.Collectors.*; long howManyDishes = menu.stream().collect(counting()); howManyDishes = menu.stream().count();6.2.1 스트림값에서 최댓값과 최솟값 검색
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories); Optional<Dish> mostCaloriesDish = menu.stream().collect(maxBy(dishCaloriesComparator)); mostCaloriesDish = menu.stream().collect(minBy(dishCaloriesComparator));6.2.2 요약 연산
- 숫자 필드의 합계나 평균등을 반환하는 연산.
합계 : summingInt, summingDouble, summingLong
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));평균 : averagingInt,averaging, averagingDouble
double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories));다중값 ( 카운터, 합계, 최소값, 평균, 최대값 ) : summarizingInt, summarizingLong, summarizingDouble
IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
6.2.3 문자열 연결
- 문자열을 join시키기..
String shortMenu = menu.stream().map(Dish::getName).collect(joing(","));6.2.4 범용 리듀싱 요약 연산
- 프로그램의 가독성을 위해 Collectors.reducing을 추천한다.
int totalCalories = menu.stream().collect( reducing( 0, Dish::getCalories,(i,j)->i+j )); - 첫 번째 인수는 리듀싱 연산의 시작값이거나 스트림에 인수가 없을 때 반환값이다.(숫자 합계에서는 인수가 없을 때 반환값으로 0이 적합하다)
- 두 번째 인수는 사용되어질 값.
세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator다.
max 값 구하기
Optional<Dish> mostCalorieDish = menu.stream() .collect(reducing( (d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2 ));reduce의 잘못된 예
Stream<Integer> stream = Arrays.asList(1, 2, 3, 4, 5, 6).stream(); List<Integer> numbers = stream.reduce( new ArrayList<Integer>(), (List<Integer> l, Integer e) -> { l.add(e); return l; }, (List<Integer> l1, List<Integer> l2) -> { l1.addAll(l2); return l1; });- 의미론적 문제 : collect 메서드는 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계된 메서드인 반면, reduce는 두 값을 하나로 도출하는 불변형 연산이라는 점에서 의미론적인 문제가 발생.
- 실용성 문제 : 여러 스레드가 동시에 같은 데이터 구조체를 고침ㄴ 리스트 자체가 망가져버리므로 리듀싱 연산을 병렬로 수행할 수 없다는 점도 문제. 이 문제를 해결하려면 매번 새로운 리스트를 할당해야 하고 따라서 객체를 할당하느라 성능이 저하될 것이다.
- 가변 컨테이너 관련 작업이면서 병렬성을 확보하려면 collect 메서드로 리듀싱 연산을 구현하는 것이 바람직 하다.
- 컬렉션 프레임워크 유연성 : 같은 연산도 다양한 방식으로 수행할 수 있다.
- 리듀싱 사용한 합계
int totalCalories = menu.stream().collect(reducing(0, //초깃값
Dish::getCalories, //변환함수
Integer::sum)); //합계함수
- counting 컬렉터도 세 개의 인수를 갖는 reducing 팩토리 메서드를 이용해서 구현 할 수 있다.
public static <T> Collector<T,?,Long>counting(){ return reducing(0L, e -> 1L, Long::sum); }int totalCalories = menu.stream().map(Dish::getCalories).reduce(Integer::sum) .get(); //Optional<Integer>를 get으로 값을 가져옴. 이것보다 orElse, orElseGet 으로 쓰는것이 좋음.int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum();
6.3 그룹화
enum CaloricLevel { DIET, NORMAL, FAT };
Map<CaloricLevel, List<Dish>> result = menu.stream().collect(
groupingBy(dish -> {
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
} ));
6.3.1 다수준 그룹화
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> result = menu.stream().collect(
groupingBy(Dish::getType,
groupingBy((Dish dish) -> {
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
} )
)
);
6.3.2 서브그룹으로 데이터 수집
- 요리 종류별 갯수
Map<Dish.Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType,counting()); - 분류별 칼로리가 가장 높은 음식
Map<Dish.Type, Optional<Dish>> mostCaloricByType = menu.stream().collect(groupingBy(Dish::getType,maxBy(comparingInt(Dish::getCalories)))); 컬렉터 결과를 다른 형식에 적용하기
Map<Dish.Type, Dish> mostCaloricByType = menu.stream() .collect( groupingBy( Dish::getType, //분류 collectingAndThen( //maxBy를 사용하면 Optional이 반환됨으로... maxBy(comparingInt(Dish::getCalories)), Optional::get)));//get메소드로 Dish를 반환받는다.groupingBy와 함께 사용하는 다른 컬렉터 예제
summingInt : 합계
Map<Dish.Type, Integer> totalCaloriesByType = menu.stream().collect(groupingBy(Dish::getType,summingInt(Dish::getCalories)));mapping : 누적
Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType =
menu.stream().collect(
groupingBy(Dish::getType, mapping(
dish -> { if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT; },
toSet() )));
caloricLevelsByType =
menu.stream().collect(
groupingBy(Dish::getType, mapping(
dish -> { if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT; },
toCollection(HashSet::new) )));
6.4 분할
- 분할 함수 : boolean을 키로 사용하는 그룹화
Map<Boolean, List<Dish>> partitionedMenu =
menu.stream().collect(partitioningBy(Dish::isVegetrian));
6.4.1 분할 장점
- 반대의 것을 얻을수 있다.
6.4.2 숫자를 소수와 비소수로 분할하기
public static Map<Boolean, List<Integer>> partitionPrimes(int n) {
return IntStream.rangeClosed(2, n).boxed()
.collect(partitioningBy(candidate -> isPrime(candidate)));
}
public static boolean isPrime(int candidate) {
return IntStream.rangeClosed(2, candidate-1)
.limit((long) Math.floor(Math.sqrt((double) candidate)) - 1)
.noneMatch(i -> candidate % i == 0);
}
- Collectors Class : http://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html
팩토리 메서드 반환형식 사용 예제 toList List<T> 스트림의 모든 항목을 리스트로 수집. List<Dish> dishes = menuStream.collect(toList()); toSet Set<T> 스트림의 모든 항목을 중복이 없는 집합으로 수집 Set<Dish> dishes = menuStream.collect(toSet()); toCollection Collection<T> 스트림의 모든 항목을 공급자가 제공하는 컬렉션으로 수집 Collection<Dish> dishes = menuStream.collect(toCollection(), ArrayList::new); counting Long 스트림의 항목 수 계산. long howManyDishes = menuStream.collect(counting()); summingInt Integer 스트림의 항목에서 정수 프로퍼티값을 더함 int totalCalories = menuStream.collect(summingInt(Dish::getCalories)); averagingInt Double 스트림 항목의 정수 프로퍼티의 평균값 계산. double avgCalories = menuSream.collect(averagingInt(Dish::getCalories)); summarizingInt IntSummaryStatistics 스트림 내의 항목의 최대값, 최솟값, 합계, 평균등의 정수 정보 통계를 수집 IntSummaryStatistics menuStatistics = menuStream.collect(summarizingInt(Dish::getCalories)); joining String 스트림의 각 항목에 toString 메서드를 호출한 결과 문자열을 연결 String shortMenu = menuStream.map(Dish::getName).collect(joining(", ")); maxBy Optional<T> 주어진 비교자를 이용해서 스트림의 최댓값 요소를 Optional로 감싼 값을 반환.스트림에 요소가 없을 때는 Optional.empty()를 반환. Optional<Dish> fattest = menuStream.collect(maxBy(comparingInt(Dish::getCalories))); minBy Optional<T> 주어진 비교자를 이용해서 스트림의 최솟값 요소를 Optional로 감싼 값을 반환. 스트림에 요소가 없을때는 Optional.empty()를 반환. Optional<Dish> lightest = menuStream.collect(minBy(comparingInt(Dish::getCalories))); reducing 리듀싱 연산에서 형식을 결정 누적자를 초깃값으로 설정한 다음에 binaryOperator로 스트림의 각 요소를 반복적으로 누적자와 합쳐 스트림을 하나의 값으로 리듀싱. int totalCalories = menuStream.collect(reducing(0,Dish::getCalories, Integer::sum)); collectingAndThen 변환 함수가 형식을 반환 다른 컬렉터를 감싸고 그 결과에 변환 함수를 적용. int howManyDishes = menuStream.collect(collectingAndThen(toList(), List::size)); groupingBy Map<K,List<T>> 하나의 프로퍼티 값을 기준으로 스트림의 항목을 그룹화하며 기준 프로퍼티값을 결과 맵의 키로 사용. Map<Dish.Type,List<Dist> dishesByType = menuStream.collect(groupingBy(Dish::getType)); partitioningBy Map<Boolean,List<T>> 프레디케이트를 스트림의 각 항목에 적용한 결과로 항목을 분할. Map<Boolean,List<Dish>> vegetarianDishes = menuStream.collect(partitioningBy(Dish::isVegetarian));
6.5 Collector 인터페이스
- http://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html
public interface Collector<T, A, R>{ Supplier<A> supplier(); BiConsumer<A,T> accoumulator(); Function<A,R> finisher(); BinaryOperator<A> combiner(); Set<Characteristics> characteristics(); } - T는 수집될 스트림 항목의 제네릭 형식이다.
- A는 누적자, 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식이다.
R은 수집연산 결과 객체의 형식(항상 그런것은 아니지만 대개 컬렉션 형식)이다.
예를 들어 Stream<T>의 모든 요소를 List<T>로 수집하는 ToListCollector<T>라는 클래스를 구현할 수 있다.
public class ToListCollector<T> implements Collector<T, List<T>, List<T>>
6.5.1 Collector 인터페이스의 메서드 살펴보기
- supplier method : 새로운 결과 컨테이너 만들기. = 빈 누적자를 생성한다.
orpublic Supplier<List<T>> supplier(){ return () -> new ArrayList<T>(); }public Supplier<List<T>> supplier(){ return ArrayList::new; } - accumulator 메소드 : 결과 컨테이너에 요소 추가하기. = 리듀싱 연산을 수행하는 함수를 반환. 누적자에 값을 추가.
orpublic BiConsumer<List<T>,T> accumulator(){ return (list, item) -> list.add(item); }public BiComsumer<List<T>,T> accumulator(){ return List::add; } - finisher 메소드 : 최종 변환값을 결과 컨테이너로 적용하기.
public Function<List<T>,List<T>> finisher(){ return Function.identity(); } - combiner 메서드 : 두 결과 컨테이너 병합 (병렬처리).
public BinaryOperator<List<T>> combiner(){ return (list1, list2) -> { list1.addAll(list2); return list1; } } - Characteristics 메서드 : : 스트림을 병렬로 리듀스할 것인지 그리고 병렬로 리듀스 한다면 어떤 최적화를 선택해야 할지 힌트를 제공한다.
- UNORDERED : 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않는다.
- CONCURRENT : 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있으며 이컬렉터는 스트림의 병렬 리듀싱을 수행 할수 있다. 컬렉터 플래그에 UNORDERED가 선언되지 않는다면 무순서 병렬 리듀싱을 수행 할 수 있다.
- IDENTITY_FINISH : finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 이를 생략할 수 있다. 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용 할수 있다. 또한 누적자 A를 결과 R로 안전하게 형 변환할 수 있다.
6.5.2 응용하기
import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
import static java.util.stream.Collector.Characteristics.*;
public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
@Override
public Supplier<List<T>> supplier() {
return () -> new ArrayList<T>(); //수집 연산의 시발점.
}
@Override
public BiConsumer<List<T>, T> accumulator() {
return (list, item) -> list.add(item); // 탐색한 항목을 누적하고 바로 누적자를 고친다.
}
@Override
public Function<List<T>, List<T>> finisher() {
return Function.identity()//i -> i; //항등 함수
}
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2); // 두번째 콘텐츠와 합쳐서 첫번째 누적자를 고친다.
return list1; // 변경된 첫 번째 누적자를 반환한다.
};
}
@Override
public Set<Characteristics> characteristics() {
// 콜렉터의 플래그를 IDENTITY_FINISH, CONCURRENT로 설정한다.
return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT));
}
}
- 사용
List<Dish> dishes = menuStream.collect(new ToListCollector<Dish>()); - 컬렉터 구현을 만들지 않고도 커스텀 수집 수행하기 (IDENTITY_FINISH와 CONCURRENT지만 UNORDERED가 아닌.)
List<Dish> dishes = menuStream.collect( ArrayList::new, //supplier List::add, // accumulator List::addAll // combiner );6.6 커스텀 컬렉터를 구현해서 성능 개선하기
6.6.1 소수로만 나누기