6.1 컬렉터란 무엇인가?
Collector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 지정한다.
6.1.1 고급 리듀싱 기능을 수행하는 컬렉터
collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의 할 수 있다는 점이 컬렉터의 최대 강점이다. 구체적으로 설명해서 스트림에 collect를 호출하면 스트림의 요소에 리듀싱 연산이 수행된다.
Collectors 유틸리티 클래스는 자주사용하는 컬렉터 인스턴스를 손쉽게 생성 할 수 있는 정적 팩토리 메서드를 제공한다.
Ex) 가장 많이 사용하는 직관적인 정적 메서드로 collect(Collectors.toList);
6.1.2 미리 정의된 컬렉터
미리 정의된 컬렉터는 groupingBy 같이 Collectors 클래스에서 제공하는 팩토리 기능을 설명한다. Collectors에서 제공하는 메서드의 기능은 크게 세가지로 구분 할 수 있다.
- 스트림 요소를 하나의 값으로 리듀스하고 요약
- 요소 그룹화
- 요소 분할
6.2 리듀싱과 요약
// Counting 메서드를 이용하여 메뉴에서 요리수를 계산
long counting1 = menu.stream().collect(Collectors.counting());
// 불팔요과정을 생략사용가능
long counting2 = menu.stream().count();
6.2.1 스트림값에서 최댓값과 최솟값 검색
/**
* 6.2.1 스트림값에서 최댓값과 최솟값 검색
*/
Comparator<Dish> dishComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> max = menu.stream().collect(Collectors.maxBy(dishComparator));
Optional<Dish> min = menu.stream().collect(Collectors.minBy(dishComparator));
6.2.2 요약 연산
Collectors 클래스는 Collectors.summingInt 라는 특별한 요약 팩토리 메서드를 제공한다.
/**
* 6.2.2 요약 연산
*/
// 총 칼로리 계산
int totalCalories = menu.stream().collect(Collectors.summingInt(Dish::getCalories));
메뉴의 칼로리 총 칼로리를 계산하는 코드이다. summingInt 외에 summingDouble, summingLong 메서드도 있다.
이러한 단순 합계 외에 평균값 계산 등의 연산도 요약 기능으로 제공된다.
// 칼로리의 평균값 계산
Double avgCalories = menu.stream().collect(Collectors.averagingInt(Dish::getCalories));
averaginInt, averagingLong, averaginDouble 등 다양한 형식으로 이루어진 숫자 집합의 평균을 계산 할 수 있다.
하나의 요약 연산으로 메뉴에 있는 수, 칼로리합계, 평균, 최댓값, 최솟값등을 계산하는 코드이다.
menu.add(new Dish("생선", false, 1200, Type.FISH));
menu.add(new Dish("돼지고기", false, 2300, Type.MEAT));
menu.add(new Dish("김치", true, 200, Type.OTHER));
menu.add(new Dish("양파", true, 300, Type.OTHER));
menu.add(new Dish("소고기", false, 1000, Type.MEAT));
IntSummaryStatistics collect = menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));
System.out.println(collect);
해당결과이다. 마찬가지로 int 뿐 아니라 long이나 double에 대응하는 summarizingLong, summarizingDouble메서드도 있다.
6.2.3 문자열 연결
Collector에 joining 팩토리 메서드를 이용하면 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다.
/**
* 6.2.3 문자열 연결
*/
String joiningNames = menu.stream().map(Dish::getName).collect(Collectors.joining());
System.out.println(joiningNames);
joining 메서드는 내부적으로 StringBuilder를 이용해서 문자열을 하나로 만든다.
// 구분자 사용
String joiningNamesSeparator = menu.stream().map(Dish::getName).collect(Collectors.joining(", "));
System.out.println(joiningNamesSeparator);
6.2.4 범용 리듀싱 요약 연산
/**
* 6.2.4 범용 리듀싱 요약 연산
*/
Integer reducing1 = menu.stream()
.collect(Collectors.reducing(0, Dish::getCalories, (a, b) -> a + b));
reducing은 인수 세개를 받는다.
- 첫 번째 인수는 리듀싱 연산의 시작 초기값이거나, 스트림에 인수가 없을때는 반환값이다.
- 두 번째 인수는 요리를 칼로리 정수로 변환할 때 사용한 변환 함수이다.
- 세 번째 인수는 같은 종류의 두항목을 하나의 값으로 더하는 BinaryOperator이다.
// 가장 칼로리가 높은 요리를 찾는 방법
Optional<Dish> reducing2 = menu.stream()
.collect(Collectors.reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
한개의 인수를 갖는 reduce
int totalCalories2 = menu.stream()
.map(Dish::getCalories).reduce(Integer::sum).get();
한개의 인수를 갖는 reduce를 스트림에 적용한 다른 예제와 마찬가지로 빈스트림과 관련한 Null 문제를 피하도록 Optional<Integer>를 반환한다.
스트림 인터페이스를 잘 사용하면 코드의 재사용성과 커스터 마이즈 가능성이 높아지고, 추상화와 일반화를 얻을 수 있다.
6.3 그룹화
자바 8의 함수형을 이용하면 가독성 있는 한줄의 코드로 그룹화를 구현 할 수 있다.
/**
* 6.3 그룹화
*/
Map<Type, List<Dish>> dishesByType = menu.stream().collect(Collectors.groupingBy(Dish::getType));
System.out.println(dishesByType);
menu.stream().collect(Collectors.groupingBy(dish ->{
if(dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT
}));
각 Key는 Dish의 Type으로 Value는 해당 값이 들어간다.
6.3.1 그룹화된 요소 조작
요소를 그룹화 한 다음에는 각 결과 그룹의 요소를 조작하는 연산
/**
* 6.3.1 그룹화된 요소 조작
*/
// 500 칼로리 이상 필터 후 그룹핑
Map<Integer, List<Dish>> caloricDishesByType = menu.stream().filter(dish -> dish.getCalories() >= 500)
.collect(Collectors.groupingBy(Dish::getCalories));
위 코드의 단점으로 메뉴 요리는 맵 형태로 되어있는데, 해당 필터 타입에 맞지않는다면 키가 사라진다.
다음과 같이 수정 할 수 있다.
Map<Integer, List<Dish>> caloricDishesByType2 = menu.stream().collect(Collectors.groupingBy(Dish::getCalories,
Collectors.filtering(dish -> dish.getCalories() > 500, Collectors.toList())));
System.out.println("caloricDishesByType1 = " + caloricDishesByType1);
System.out.println("caloricDishesByType2 = " + caloricDishesByType2);
6.3.2 다수준 그룹화
/**
* 6.3.2 다수준 그룹화
*/
Map<Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = menu.stream().collect(
groupingBy(Dish::getType, // 첫번째 수준의 분류함수
groupingBy(dish -> { // 두번째 수준의 분류함수
if (dish.getCalories() <= 400)
return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) {
return CaloricLevel.NORMAL;
} else return CaloricLevel.FAT;
})));
System.out.println("dishesByTypeCaloricLevel = " + dishesByTypeCaloricLevel);
dishesByTypeCaloricLevel = {FISH={DIET=[Dish(name=prawns, vegetarian=false, calories=300, type=FISH)]}, OTHER={NORMAL=[Dish(name=french fries, vegetarian=true, calories=480, type=OTHER)], DIET=[Dish(name=seasonal fruit, vegetarian=true, calories=120, type=OTHER), Dish(name=rice, vegetarian=true, calories=350, type=OTHER)]}, MEAT={DIET=[Dish(name=chicken, vegetarian=false, calories=400, type=MEAT)]}}
외부 맵은 첫번째 수준의 분류함수에서 분류한 키값 FISH, MEAT, OTHER로 나누고, 두번째 수준의 분류함수의 NORMAL, DIET, FAT을 키값으로 갖는다.
6.3.3 서브그룹으로 데이터 수집
groupingBy 컬렉터를 이용한 count
/**
* 6.3.3 서브그룹으로 데이터 수집
*/
Map<Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType, counting()));
System.out.println("typesCount = " + typesCount);
typesCount = {FISH=1, OTHER=3, MEAT=1}
요리의 종류 별 칼로리가 가장 높은 요리 출력
Map<Type, Optional<Dish>> mostCaloricByType = menu.stream().collect(
groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));
System.out.println("mostCaloricByType = " + mostCaloricByType);
컬렉터 결과를 다른 형식에 적용하기
마지막 그룹화 연산에서 맵의 모든 값을 Optional로 감쌀 필요가 없으므로 Optional을 삭제 할 수 있다.
// Optional 삭제
Map<Type, Dish> mostCaloricByTypeNotOptional = menu.stream().collect(groupingBy(Dish::getType,
collectingAndThen(maxBy(comparingInt(Dish::getCalories)),
Optional::get)));
System.out.println("mostCaloricByTypeNotOptional = " + mostCaloricByTypeNotOptional);
팩토리 메서드 collectionAndThen은 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환한다.