Java8 In Action - 6. 스트림으로 데이터 수집


Java8 In Action 을 읽고 정리한 내용이다. Collectors 클래스로 컬렉션을 만들고 사용하는 방법에 대해서 설명한다.

리듀싱과 요약

요약 연산

summingXXX

Collectors 클래스는 Collectors.summingInt 라는 특별한 요약 메서드를 제공한다. 각 리스트에 선택된 값들을 합한 값으로 리턴한다.

Dish.java
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
@Getter @Setter@ToString
@AllArgsConstructor
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.summingLongCollectors.summingDoubleCollectors.summingInt와 같은 방식으로 동작한다.

averagingXXX

평균값 계산 등의 연산도 요약 기능으로 제공된다.

Reducing.java
1
2
3
4
5
6
7
8
9
10
11
public 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를 이용해서 문자열을 하나로 만든다.

Reducing.java
1
2
3
4
5
6
7
8
public 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 팩토리 메서드로도 정의 할 수 있다.

Reducing.java
1
2
3
4
5
6
7
8
public 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은 세 개의 인수를 받는다.

  1. 첫 번째 인수는 리듀싱 연산의 시작값이거나 스트림에 인수가 없을 때는 반환값이다(숫자 합계에서는 인수가 없을 때 반환값으로 0이 적합하다).
  2. 두 번째 인수는 데이터 값을 정수로 변환할 떄 사용한 변환 함수다.
  3. 세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator다. 위에서는 두 개의 int가 사용 되었다.

한 개의 인수를 가진 reducing 가장 높은 값들을 찾을수 있다.

Reducing.java
1
2
3
4
5
6
7
8
public 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를 이용해서 항목을 다수준으로 그룹화 할 수 있다.

Grouping.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public 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 dishes = menuStream.collect(toList());
toSet Set 스트림의 모든 항목을 중복이 없는 집합으로 수집 Set dishes = menuStream.collect(toSet());
toCollection Collection 스트림의 모든 항목을 공급자가 제공하는 컬렉션으로 수집 Collection 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 = 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 lightest = menuStream.collect(maxBy(comparingInt(Dish::getCalories)));
minBy Optional 주어진 비교자를 이용해서 스트림의 최솟값 요소를 Optional로 감싼 값을 반환. 스트림에 요소가 없을 때는 Optional.empty()를 반환 Optional lightest = menuStream.collect(minBy(comparingInt(Dish::getCalories)));
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> dishesByType = menuStream.collect(groupingBy(Dish::getType));
partitionBy Map<Boolean, List> 프레디케이트를 스트림의 각 항복에 적용한 결과로 항목을 분할 Map<Boolean, List> vegetarianDishes = menuStream.collect(partitioningBy(Dish::isVegetarian));

Collector 인터페이스

Collector 인터페이스 살펴 보기

Collector.java
1
2
3
4
5
6
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Characteristics> characteristics();

  1. T는 수집될 스트림 항목의 제네릭 형식이다.
  2. A는 누적자, 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식이다.
  3. 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
3
public Supplier<List<T>> supplier() {
return () -> new ArrayList<T>(); // 수집 연산의 시발점
}

accumulator 메서드: 결과 컨테이너에 요소 추가하기

accumulator 메서드는 리듀싱 연산을 수행하는 함수를 반환한다. 스트림의 n번째 연산을 탐색할 때 두 인수, 즉 누적자(스트림의 첫 n-1개 항목을 수집한 상태)와 n번째 요소를 함수에 적용한다.

1
2
3
public BiConsumer<List<T>, T> accumulator() {
return (list, item) -> list.add(item); // 탐색한 항목을 누적하고 바로 누적자를 고친다.
}

finisher 메서드: 최종 변환값을 결과 컨테이너로 적용하기

finisher 메서드는 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 반환하면서 누적 과정을 끝낼 때 호출할 함수를 반환해야 한다.

1
2
3
public Function<List<T>, List<T>> finisher() {
return i -> i; // 항등 함수
}

combiner 메서드: 두 결과 컨테이너 병합

combiner는 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의한다.

1
2
3
4
5
6
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2); // 두 번째 콘텐츠와 함쳐서 첫 번째 누적자를 고친다.
return list1;
};
}

Characteristics 메서드

characteristics 메서드는 컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환한다.

  • UNORDERED 이듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않는다.
  • CONCURRENT 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있으며 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다.
  • IDENTITIY_FINISH finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 이를 생략할 수 있다.
1
2
3
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT)); // 콜렉터의 플래그를 IDENTITY_FINISH.CONCURRENT로 설정한다.
}

소스코드

참조