Java8 In Action 을 읽고 정리한 내용이다. Collectors 클래스로 컬렉션을 만들고 사용하는 방법에 대해서 설명한다.
리듀싱과 요약
요약 연산
summingXXX
Collectors
클래스는 Collectors.summingInt
라는 특별한 요약 메서드를 제공한다. 각 리스트에 선택된 값들을 합한 값으로 리턴한다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private final Type type;
public enum Type {
MEAT, FISH, OTHER
}
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));
}
```java Reducing.java
public class Reducing {
public static void main(String[] args) {
int totalCalories = Dish.menu.stream().collect(summingInt(dish -> dish.getCalories()));
System.out.println(totalCalories);
}
}
결과 화면1
4300
Collectors.summingLong
과Collectors.summingDouble
도Collectors.summingInt
와 같은 방식으로 동작한다.
averagingXXX
평균값 계산 등의 연산도 요약 기능으로 제공된다.1
2
3
4
5
6
7
8
9
10
11public class Reducing {
public static void main(String[] args) {
int totalCalories = Dish.menu.stream().collect(summingInt(dish -> dish.getCalories()));
System.out.println(totalCalories);
double avgCalories = Dish.menu.stream().collect(averagingInt(dish -> dish.getCalories()));
System.out.println(avgCalories);
}
}
결과 화면1
477.77777777777777
averagingInt
,averagingLong
,averagingDouble
등으로 다양한 형식으로 이루어진 숫자 집합의 평균을 계산할 수있다.
문자열 연결
컬렉터에 joining
팩토리 메서드를 이용하면 스트림의 각 객체에 toString
메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다. joining
메서드는 내부적으로 Stringbuilder
를 이용해서 문자열을 하나로 만든다.1
2
3
4
5
6
7
8public class Reducing {
public static void main(String[] args) {
String shortMenu = Dish.menu.stream().map(dish -> dish.getName()).collect(joining(", "));
System.out.println(shortMenu);
}
}
결과 화면1
pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon
범용 리듀싱 요약 연산
범용 Collectors.reducing
으로 지금까지 살펴본 모든 컬렉터를 reducing
팩토리 메서드로도 정의 할 수 있다.1
2
3
4
5
6
7
8public class Reducing {
public static void main(String[] args) {
int reducingTotalCalories = Dish.menu.stream().collect(reducing(0, dish -> dish.getCalories(), (i, j) -> i + j));
System.out.println(reducingTotalCalories);
}
}
결과 화면1
4300
reducing
은 세 개의 인수를 받는다.
- 첫 번째 인수는 리듀싱 연산의 시작값이거나 스트림에 인수가 없을 때는 반환값이다(숫자 합계에서는 인수가 없을 때 반환값으로 0이 적합하다).
- 두 번째 인수는 데이터 값을 정수로 변환할 떄 사용한 변환 함수다.
- 세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는
BinaryOperator
다. 위에서는 두 개의int
가 사용 되었다.
한 개의 인수를 가진 reducing
가장 높은 값들을 찾을수 있다.1
2
3
4
5
6
7
8public class Reducing {
public static void main(String[] args) {
Optional<Dish> mostCaloriesDish = Dish.menu.stream().collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
System.out.println(mostCaloriesDish);
}
}
결과 화면1
Optional[Dish(name=pork, vegetarian=false, calories=800, type=MEAT)]
그룹화
데이터 집합을 하나 이상의 특성으로 분류해서 그룹화하는 연산도 데이터베이스에서 많이 수행되는 작업이다. 자바8의 함수형을 이용하면 가독성 있는 한 줄의 코드로 그룹화를 구현할 수 있다.
다수준 그룹화
두 인수를 받는 팩토리 메서드 Collectors.groupingBy
를 이용해서 항목을 다수준으로 그룹화 할 수 있다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class Grouping {
enum CaloricLevel { DIET, NORMAL, FAT };
public static void main(String[] args) {
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByCaloricLevel = Dish.menu.stream().collect(
groupingBy(dish -> 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(dishesByCaloricLevel);
}
}
결과 화면1
2
3{MEAT={DIET=[Dish(name=chicken, vegetarian=false, calories=400, type=MEAT)], FAT=[Dish(name=pork, vegetarian=false, calories=800, type=MEAT)], NORMAL=[Dish(name=beef, vegetarian=false, calories=700, type=MEAT)]}
,FISH={DIET=[Dish(name=prawns, vegetarian=false, calories=400, type=FISH)], NORMAL=[Dish(name=salmon, vegetarian=false, calories=450, type=FISH)]}
,OTHER={DIET=[Dish(name=rice, vegetarian=true, calories=350, type=OTHER), Dish(name=season fruit, vegetarian=true, calories=120, type=OTHER)], NORMAL=[Dish(name=french fries, vegetarian=true, calories=530, type=OTHER), Dish(name=pizza, vegetarian=true, calories=550, type=OTHER)]}}
Collectors 클래스의 정적 팩토리 메서드
팩토리 메서드 | 반환형식 | 사용 예제 | 활용 예 |
---|---|---|---|
toList | List |
스트림의 모든 항목을 리스트로 수집 | List |
toSet | Set |
스트림의 모든 항목을 중복이 없는 집합으로 수집 | Set |
toCollection | Collection |
스트림의 모든 항목을 공급자가 제공하는 컬렉션으로 수집 | Collection |
counting | Long | 스트림의 항목 수 계산 | long howManyDishes = menuStream.collect(counting()); |
summingInt | Integer | 스트림의 항목에서 정수 프로퍼티값을 더함 | int totalCalories = menuStream.collect(summingInt(Dish::getCalories)); |
averagingInt | Double | 스트림 항목의 정수 프로퍼티의 평균값 계산 | double avgCalories = menuStream.collect(averagingInt(Dish::getCalories)); |
summarizing | IntSummaryStatistics | 스트림 내의 항목의 최댓값, 최솟값, 합계, 평균 등의 정수 정보 통계를 수집 | IntSummaryStatistics menuStatistics = menuStream.collect(summarizingInt(Dish::getCalories)); |
joining | String | 스트림의 각 항목에 toString 메서드를 호출한 결과 문자열을 연결. | String shortMenu = menuStream.map(Dish::getName).collect(joining(“, “)); |
maxBy | Optional |
주어진 비교자를 이용해서 스트림의 최댓값 요소를 Optional로 감싼 값을 반환. 스트림에 요소가 없을 때는 Optional.empty()를 반환 | Optional |
minBy | Optional |
주어진 비교자를 이용해서 스트림의 최솟값 요소를 Optional로 감싼 값을 반환. 스트림에 요소가 없을 때는 Optional.empty()를 반환 | Optional |
reducing | 리듀싱 연산에서 형식을 결정 | 누적자를 초깃값으로 설정한 다음에 BinaryOperator로 스트림의 각 요소를 반복적으로 누적자와 합쳐 스트림을 하나의 값으로 리듀싱 | int totalCalories = menuStream.collect(reducing(0, Dish::getCalories, Integer::sum)); |
collectionAndThen | 변환 함수가 형식을 변환 | 다른 컬렉터를 감싸고 그 결과에 변환 함수를 적용 | int howManyDishes = menuStream.collect(collectingAndThen(toList(), List::size)); |
groupingBy | Map<K, List |
하나의 프로퍼티값을 기준으로 스트림의 항목을 그룹화하며 기준 프로퍼티값을 결과 맵의 키로 사용 | Map<Dish.Type, List |
partitionBy | Map<Boolean, List |
프레디케이트를 스트림의 각 항복에 적용한 결과로 항목을 분할 | Map<Boolean, List |
Collector 인터페이스
Collector 인터페이스 살펴 보기1
2
3
4
5
6public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Characteristics> characteristics();
- T는 수집될 스트림 항목의 제네릭 형식이다.
- A는 누적자, 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식이다.
- R은 수집 연산 결과 객체의 형식(항상 그런 것은 아니지만 대게 컬렉션 형식)이다.
예를 들어 Stream<T>
의 모든 요소를 List<T>
로 수집하는 ToListCollector<T>
라는 클래스를 구현할 수 있다.1
public class ToListCollector<T> implements Collector<T, List<T>, List<T>>
Collector 인터페이스 메서드 살펴보기
supplier 메서드: 새로운 결과 컨테이너 만들기
suplier 메서드는 빈 결과로 이루어진 Supplier를 반환해야 한다. 즉 supplier는 수집과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수다.1
2
3public Supplier<List<T>> supplier() {
return () -> new ArrayList<T>(); // 수집 연산의 시발점
}
accumulator 메서드: 결과 컨테이너에 요소 추가하기
accumulator 메서드는 리듀싱 연산을 수행하는 함수를 반환한다. 스트림의 n번째 연산을 탐색할 때 두 인수, 즉 누적자(스트림의 첫 n-1개 항목을 수집한 상태)와 n번째 요소를 함수에 적용한다.1
2
3public BiConsumer<List<T>, T> accumulator() {
return (list, item) -> list.add(item); // 탐색한 항목을 누적하고 바로 누적자를 고친다.
}
finisher 메서드: 최종 변환값을 결과 컨테이너로 적용하기
finisher 메서드는 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 반환하면서 누적 과정을 끝낼 때 호출할 함수를 반환해야 한다.1
2
3public Function<List<T>, List<T>> finisher() {
return i -> i; // 항등 함수
}
combiner 메서드: 두 결과 컨테이너 병합
combiner는 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의한다.1
2
3
4
5
6public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2); // 두 번째 콘텐츠와 함쳐서 첫 번째 누적자를 고친다.
return list1;
};
}
Characteristics 메서드
characteristics 메서드는 컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환한다.
- UNORDERED 이듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않는다.
- CONCURRENT 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있으며 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다.
- IDENTITIY_FINISH finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 이를 생략할 수 있다.
1 | public Set<Characteristics> characteristics() { |