반응형
본 내용은 라울-게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로프트의 "모던 자바 인 액션" 책을 보고 정리한 내용입니다. 저작권 보호를 위해 책의 내용은 요약되었습니다.
컬렉터란?
- 스트림의 결과를 수집하여 다양한 형태로 변환하는 역할을 한다.
- 함수형 프로그래밍 시 유리하다.
- 명시적이므로 결과를 수집하는 과정에서 간단하고 유연한 방식으로 정의할 수 있다.
- 스트림에 collect를 호출하면 내부적으로 리듀싱 연산이 수행된다.
리듀싱 연산이란?
- 리듀싱 연산이란 스트림의 모든 요소를 하나의 값으로 결합하느 과정을 나타낸다.
- 여러 요소를 단일 값으로 축소(reduce)할 수 있다.
Collectors 클래스에서 제공하는 팩토리 메서드
- 스트림 요소를 하나의 값으로 리듀스하고 요약
- 요소 그룹화
- 요소 분할
리듀싱과 요약
- Collector로 스트림의 할목을 컬렉션으로 재구성할 수 있다.
스트림값에서 최댓값 최솟값 검색
- Collector.maxBy, Collector.minBy 두개의 메서드를 사용하여 최댓값과 최솟값을 계산할 수 있다.
- 두 컬렉터는 스트림 요소를 비교하는게 사용할 Comparator를 인수로 받는다.
예시 1) Dish의 칼로리 최댓값
public static final List<Dish> menu = Arrays.asList(
new Dish("pork", false, 800,Dish.Type.MEAT),
new Dish("beef", false, 700, Dish.Type.MEAT),
new Dish("chicken", false, 400, Dish.Type.MEAT),
new Dish("french fries", true, 530, Dish.Type.OTHER),
new Dish("rice", true, 350, Dish.Type.OTHER),
new Dish("season fruit", true, 120, Dish.Type.OTHER),
new Dish("pizza", true, 550, Dish.Type.OTHER),
new Dish("prawns", false, 400, Dish.Type.FISH),
new Dish("salmon", false, 450, Dish.Type.FISH)
);
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
// Collectors.maxBy 메서드를 활용하여 칼로리 최댓값 조회
Optional<Dish> maxCalorie = menu.stream().collect(Collectors.maxBy(dishCaloriesComparator));
요약 연산
Collectors.summingInt
- 객체를 int로 매핑하여 누적 합계를 계산해준다.
- summingLong과 summingDouble은 각각 long 또는 double 형식의 데이터로 요약한다.
예시 2) summingInt를 활용한 칼로리 총합 구하기
// 칼로리 총합 구하기
int totalCalorie = menu.stream().collect(Collectors.summingInt(Dish::getCalories));
System.out.println(totalCalorie); // 출력 결과 4300;
Collectors.avaragingInt
- 평균을 위한 요약 연산을 제공한다.
- avaragingLong, avaragingDouble 메서드로 다양한 령식으로 이루어진 숫자 집합의 평균을 구할 수 있다.
예시 3) averagingInt를 활용한 칼로리 평균 구하기
// 칼로리 평균 구하기
double avgCalorie = menu.stream().collect(Collectors.averagingInt(Dish::getCalories));
System.out.println(avgCalorie); // 출력 결과 477.77777777777777
Collectors.summarizingInt
- Collectors.summarizingInt는 한번의 collect API로 합계, 평균 등의 값을 한번에 수행해야 할 경우 사용하는 메서드이다.
- 요소 수 , 합계, 평균 ,최댓값, 최솟값 등을 계산해준다.
예시 4) 칼로리의 수, 합계, 평균, 평균 ,최댓값, 최솟값 구하기
// 요소수, 합계, 최댓값, 최솟값, 평균 조회
IntSummaryStatistics summarizeCalorie = menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));
System.out.println(summarizeCalorie); // 출력 결과 IntSummaryStatistics{count=9, sum=4300, min=120, average=477.777778, max=800}
문자열 연결
Collectors.joining
- 스트림의 각 객체에 toString 메서드를 호출해 추출한 문자열을 하나의 문자열로 연결해 반환한다.
- 내부적으로 StringBuilder를 이용해 하나의 문자열로 만든다.
- 두 요소 사이에 구분 문자열을 넣을 수 있다.
예시 5) 음식이름 나열하기
String names = menu.stream().map(Dish::getName).collect(Collectors.joining(" ,"));
System.out.println(names);
// 출력결과 : pork ,beef ,chicken ,french fries ,rice ,season fruit ,pizza ,prawns ,salmon
범용 리듀싱 요약 연산
- 스트림의 요소를 감소시키는데 사용한다.
- 세개의 인수를 받는다.
- 첫 번째 인수는 리듀싱 연사의 시작값이거나 스트림에 인수가 없을 때는 반환값이다.
- 두 번째 인수는 요소를 변환할 변환 함수이다.
- 세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOprator다.
예시 6) 칼로리의 누적합을 구한다.
int totalCalorie = menu.stream().collect(Collectors.reducing(0, // 초깃값
Dish::getCalories, // 변환함수
Integer::sum)); // 합계함수
System.out.println(totalCalorie); // 출력결과 : 4300
그룹화
- Collectors.groupingBy 를 이용하여 간편하게 그룹화할 수 있다.
- 그룹화 연산의 결과로 그룹화 함수가 반환하는 키, 그리고 대응하는 항목 리스트를 값으로 갖는 맵이 반환된다.
- 다양한 Collector 인터페이스의 정적 팩토리 메서드로 복잡한 컬렉터를 만들거나 복합적인 컬렉팅 로직을 구현할 수 있다.
예시 7) 칼로리 별 음식 요소 그룹화
Map<CalorieLevel, List<Dish>> calorieLevelMap = menu.stream().collect(Collectors.groupingBy(dish->{
if(dish.getCalories() <= 400) {
return CalorieLevel.DIET;
} else if (dish.getCalories() <= 600) {
return CalorieLevel.NORMAL;
} else {
return CalorieLevel.FAT;
}
}));
System.out.println((calorieLevelMap));
/*
출력결과
{
DIET=[chicken, rice, season fruit, prawns],
NORMAL=[french fries, pizza, salmon],
FAT=[pork, beef]
}
*/
// 칼로리별 레벨
public enum CalorieLevel {DIET, NORMAL, FAT}
그룹화된 요소 조작
Collectors.filtering
- 각 그룹의 요소와 필터링 된 요소를 재그룹화 한다.
- filter 메서드와 다른점은 filtering 메서드 사용 시 목록이 비어있는 키도 반환할 수 있다.
예시 8) filter 메서드와 filtering 메서드 차이
// collect 메서드 사용
Map<Dish.Type, List<Dish>> calorieMap = menu.stream()
.filter(dish -> dish.getCalories() > 500)
.collect(Collectors.groupingBy(Dish::getType));
System.out.println(calorieMap);
/*
출력
{
OTHER=[french fries, pizza],
MEAT=[pork, beef]
}
*/
// Collectors.filtering 메서드 사용
Map<Dish.Type, List<Dish>> calorieFilterMap = menu.stream()
.collect(Collectors.groupingBy(Dish::getType,
Collectors.filtering(dish -> dish.getCalories() > 500,
Collectors.toList())));
System.out.println(calorieFilterMap);
/*
출력
{
OTHER=[french fries, pizza],
FISH=[], MEAT=[pork, beef]
}
*/
Collectors.mapping
- 그룹화된 요소를 매핑하여 새로운 형태로 변환한 후에 컬렉션으로 반환한다.
예시 9) 음식 종류 별 칼로리를 구한다.
// 음식 종류별 칼로리를 구한다.
Map<Dish.Type, List<Integer>> calorieMappintMap = menu.stream()
.collect(Collectors.groupingBy(Dish::getType,
Collectors.mapping(Dish::getCalories,
Collectors.toList())));
System.out.println(calorieMappintMap);
/*
출력
{
FISH=[400, 450], MEAT=[800, 700, 400],
OTHER=[530, 350, 120, 550]
}
*/
Collectors.flatMapping
- 각 요소를 매핑한 후에 그 결과를 하나의 평면화된 스트림으로 평탄화하여 수집합니다.
예시 10) 음식 종류별 음식 태그리스트를 구한다.
// 음식 태그
public static final Map<String, List<String>> dishTags = new HashMap<>();
static {
dishTags.put("pork", asList("greasy", "salty"));
dishTags.put("beef", asList("salty", "roasted"));
dishTags.put("chicken", asList("fried", "crisp"));
dishTags.put("french fries", asList("greasy", "fried"));
dishTags.put("rice", asList("light", "natural"));
dishTags.put("season fruit", asList("fresh", "natural"));
dishTags.put("pizza", asList("tasty", "salty"));
dishTags.put("prawns", asList("tasty", "roasted"));
dishTags.put("salmon", asList("delicious", "fresh"));
}
// 음식 종류별 음식 태그리스트를 구한다.
Map<Dish.Type, Set<String>> nameMappintMap = menu.stream()
.collect(Collectors.groupingBy(Dish::getType,
Collectors.flatMapping(dish -> dishTags.get(dish.getName()).stream(), Collectors.toSet()))); // 태그에서 이름을 키로 값을 꺼내어 set으로 변환
System.out.println(nameMappintMap);
/* 출력
{
FISH=[roasted, tasty, fresh, delicious],
MEAT=[salty, greasy, roasted, fried, crisp],
OTHER=[salty, greasy, natural, light, tasty, fresh, fried]
}
*/
다수준 그룹화
Collectors.groupingBy
- groupingBy 메서드를 중첩하여 다수준 그룹화를 수행할 수 있다.
- 스트림 항목을 분류할 두 번째 기준을 정의하는 내부 groupingBy 를 전달해서 두 수준으로 스트림의 항목을 그룹화 할 수 있다.
- 반복하면 n 수준 그룹화도 가능하다.
예시 11) 첫 번째는 음식 종류는 분류하고, 두 번째는 칼로리로 분류한다.
Map<Dish.Type, Map<CalorieLevel, List<Dish>>> calorieGroupingMap = menu.stream().collect(
Collectors.groupingBy(Dish::getType,
Collectors.groupingBy(dish -> {
if(dish.getCalories() <= 400){
return CalorieLevel.DIET;
} else if(dish.getCalories() <= 700) {
return CalorieLevel.NORMAL;
} else {
return CalorieLevel.FAT;
}
})
)
);
System.out.println(calorieGroupingMap);
/* 출력
{
FISH={DIET=[prawns], NORMAL=[salmon]},
MEAT={FAT=[pork], DIET=[chicken], NORMAL=[beef]}, OTHER={DIET=[rice, season fruit],
NORMAL=[french fries, pizza]}
}
*/
서브그룹으로 데이터 수집
- groupingBy 컬렉터의 두번째 인수로 다른 컬렉터를 전달해 다른 형식의 값을 수집할 수 있다.
예시 12) 음식 종류별로 그룹화하여 칼로리가 최대인 음식을 구한다.
Map<Dish.Type, Optional<Dish>> maxcalorieMap = menu.stream().collect(
Collectors.groupingBy(Dish::getType,
Collectors.maxBy(Comparator.comparingInt(Dish::getCalories))));
System.out.println(maxcalorieMap);
/*
출력
{
FISH=Optional[salmon],
MEAT=Optional[pork],
OTHER=Optional[pizza]
}
*/
컬렉터 결과를 다른 형식에 적용하기
Collectors.collectingAndThen
- 스트림 요소를 수집한 다음에 추가적인 변환을 적용할 수 있는 유용한 유틸리티 메서드
예시 13) 음식 종류별로 그룹화하여 칼로리가 최대인 음식을 수집하고 Optional에 포함된 값을 추출한다.
Map<Dish.Type, Dish> maxcalorieMap = menu.stream().collect(
Collectors.groupingBy(Dish::getType,
Collectors.collectingAndThen(Collectors.maxBy(
Comparator.comparingInt(Dish::getCalories)),
Optional::get)));
System.out.println(maxcalorieMap);
/*
출력
{
OTHER=pizza, FISH=salmon,
MEAT=pork
}
*/
toCollection
- 스트림 요소를 수집(collect)하여 원하는 유형의 컬렉션에 저장하는 데 사용
예시 14) 요리의 종류에 따라 그룹화하고 각 그룹 내에서 칼로리 수준을 매핑하여 Set 형태로 수집한다.
Map<Dish.Type, Set<CalorieLevel>> calorieSet = menu.stream().collect(
Collectors.groupingBy(Dish::getType, Collectors.mapping(dish -> {
if (dish.getCalories() <= 400) {
return CalorieLevel.DIET;
} else if (dish.getCalories() <= 700) {
return CalorieLevel.NORMAL;
} else {
return CalorieLevel.FAT;
}
}, Collectors.toCollection(HashSet::new))));
System.out.println(calorieSet);
/*
출력
{
FISH=[DIET, NORMAL],
MEAT=[FAT, DIET, NORMAL],
OTHER=[DIET, NORMAL]
}
*/
분할
- 분할 함수를 이용하여 스트림의 요소를 조건에 따라 두 그룹으로 나누는 작업을 말한다.
- Collectior.partitioningBy
- 스트림의 요소를 주어진 조건에 따라 두 개의 그룹으로 분할하고, 각 그룹을 true와 false의 키를 가진 Map으로 수집한다.
- groupingBy와 같이 사용할 수도 있고 이 메소드 또한 중복이 가능하다.
예시 15) partitioningBy 메소드 사용하여 메뉴 분할
// 채식은 true 이고 나머지는 false
Map<Boolean, List<Dish>> partitionMap = menu.stream()
.collect(Collectors.partitioningBy(Dish::isVegetarian));
System.out.println(partitionMap);
/* 출력
{
false=[pork, beef, chicken, prawns, salmon],
true=[french fries, rice, season fruit, pizza]}
*/
// 채식은 true 이고 나머지는 false 그리고 음식 종류별로 그룹화
Map<Boolean, Map<Dish.Type,List<Dish>>> groupingMap = menu.stream()
.collect(Collectors.partitioningBy(Dish::isVegetarian,
Collectors.groupingBy(Dish::getType)));
System.out.println(groupingMap);
/* 출력
{
false={FISH=[prawns, salmon], MEAT=[pork, beef, chicken]},
true={OTHER=[french fries, rice, season fruit, pizza]}
}
*/
// 채식은 true 이고 나머지는 false 그리고 칼로리가 500 이상만 true
Map<Boolean, Map<Boolean,List<Dish>>> partitionMap2 = menu.stream()
.collect(Collectors.partitioningBy(Dish::isVegetarian,
Collectors.partitioningBy(dish -> dish.getCalories() > 500)));
System.out.println(partitionMap2);
/* 출력
{
false={
false=[chicken, prawns, salmon],
true=[pork, beef]
},
true={
false=[rice, season fruit],
true=[french fries, pizza]
}
}
*/
반응형
'Java' 카테고리의 다른 글
[자바8] 스트림 활용 (2) | 2024.01.22 |
---|---|
[자바8] 스트림 (0) | 2023.12.30 |
[자바 8] 메서드 참조 (0) | 2023.09.24 |
[자바 8] 함수형 인터페이스 (0) | 2023.09.24 |
[자바 8] 람다 표현식 (0) | 2023.09.18 |