뒤늦게 배워보자, Java 8 Part. 4
이 문서는 자바 8 인 액션 - 람다, 스트림, 함수형 프로그래밍으로 새로워진 자바 마스터하기와 Functional Programming in Java 8 - 자바 8 람다의 힘을 참고하였습니다. 개인적으로 자바 8 인 액션을 추천드리며, 해당 기사에서 사용한 모든 예제는 자바 8 인 액션을 발췌, 수정하였습니다.
리듀싱과 요약
Collectors.maxBy, Collectors.minBy 두 개의 메서드를 이용해서 스트림의 최댓값과 최솟값을 계산할 수 있습니다. 두 컬렉터는 스트림의 요소를 비교하는데 사용할 Comparator를 인수로 받습니다. 또한 스트림에 있는 객체의 숫자 필드의 합계(summingInt)나 평균(averagingInt) 등을 반환하는 연산에도 리듀싱 기능이 자주 사용되며, 이러한 연산을 요약 연산이라고 부릅니다.
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
menu.stream().collect(summingInt(Dish::getCalories); // 합
menu.stream().collect(averagingInt(Dish::getCalories); // 평균
private static IntSummaryStatistics calculateMenuStatistics() {
return menu.stream().collect(summarizingInt(Dish::getCalories)); // 스트림 요약
}
private static String getShortMenuCommaSeparated() {
return menu.stream().map(Dish::getName).collect(joining(", ")); // 문자열 연결
}
범용 리듀싱은 초기값, 변환함수, 람다 세개의 인수를 필요로 하고, 초기값이 없이 람다식으로만 이뤄진 범용 리듀싱의 경우 변환값이 Optional로 반환됩니다.
// 범용 리듀싱 1
menu.stream().collect(reducing(0, Dish::getCalories, (Integer i, Integer j) -> i + j));
// 범용 리듀싱 2
menu.stream().collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2)).get();
그룹화
데이터 집합을 하나 이상의 특성으로 분류해서 그룹화하는 연산도 데이터베이스에서 많이 수행되는 작업입니다. 함수형을 이용하면 가독성 있는 한 줄의 코드로 그룹화할 수 있는 일들이 많습니다. 단순한 속성 접근자 대신 더 복잡한 분류 기준이 필요한 상황에서는 메서드 레퍼런스가 아니라 람다 표현식으로 구현할 수 있습니다. Dish.Type과 일치하는 모든 요리를 추출하는 함수를 groupingBy 메서드로 전달하였습니다. 이것을 분류 함수라고 부릅니다.
private static Map<Dish.Type, List<Dish>> groupDishesByType() {
return menu.stream().collect(groupingBy(Dish::getType));
}
enum CaloricLevel {DIET, NORMAL, FAT}
private static Map<CaloricLevel, List<Dish>> groupDishesByCaloricLevel() {
return menu.stream().collect(
groupingBy(dish -> {
if (dish.getCalories() <= 400) {
return CaloricLevel.DIET;
} else if (dish.getCalories() <= 700) {
return CaloricLevel.NORMAL;
} else {
return CaloricLevel.FAT;
}
})
);
}
다수준 그룹화
아래 코드는 다수준으로 그룹화 할 수 있습니다. 앞서본 그룹화를 한 번 더 적용하면 몇수준의 그룹화도 가능합니다.
private static Map<Dish.Type, Map<CaloricLevel, List<Dish>>> groupDishedByTypeAndCaloricLevel() {
return 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;
}
})
)
);
}
서브그룹
분류 함수 한 개의 인수를 갖는 groupingBy(f)는 사실 groupingBy(f, toList())의 축양형이라 할 수 있습니다. 만약 마지막에 반환하는 형태가 Optional이 필요가 없을 경우 collectingAndThen으로 컬렉터가 반환한 결과를 다른 형식으로 활용할 수 있습니다.
private static Map<Dish.Type, Optional<Dish>> mostCaloricDishesByType() {
return menu.stream().collect(
groupingBy(Dish::getType,
reducing((Dish d1, Dish d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2)));
}
private static Map<Dish.Type, Dish> mostCaloricDishesByTypeWithoutOprionals() {
return menu.stream().collect(
groupingBy(Dish::getType,
collectingAndThen(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2), Optional::get)
)
);
}
분할
분할은 분할 함수라 불리는 프레디케이트를 분류 함수로 사용하는 특수화 그룹화 기능입니다. 분할 함수는 불린을 반환하므로 맵의 키 형식은 Boolean입니다. 결과적으로 그룹화 맵은 최대 두 개의 그룹으로 분류됩니다. 분할 함수가 반환하는 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다는 것이 분할의 장점입니다.
private static Map<Boolean, List<Dish>> partitionByVegeterian() {
return menu.stream().collect(partitioningBy(Dish::isVegetarian)); // 분할함수
}
Collector 인터페이스
supplier는 새로운 결과 컨테이너를 생성accumulator는 결과 컨테이너에 요소를 추가finisher는 최종 변환값을 결과 컨테이너로 적용combiner는 두 결과 컨테이너를 병합characteristics메서드는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 i -> i;
}
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
};
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT));
}
}
커스텀 컬렉터
public Map<Boolean, List<Integer>> partitionPrimesWithInlineCollector(int n) {
return Stream.iterate(2, i -> i + 1).limit(n)
.collect(
() -> new HashMap<Boolean, List<Integer>>() {{
put(true, new ArrayList<Integer>());
put(false, new ArrayList<Integer>());
}},
(acc, candidate) -> {
acc.get(isPrime(acc.get(true), candidate))
.add(candidate);
},
(map1, map2) -> {
map1.get(true).addAll(map2.get(true));
map1.get(false).addAll(map2.get(false));
});
}
public static class PrimeNumbersCollector
implements Collector<Integer, Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> {
@Override
public Supplier<Map<Boolean, List<Integer>>> supplier() {
return () -> new HashMap<Boolean, List<Integer>>() {{
put(true, new ArrayList<Integer>());
put(false, new ArrayList<Integer>());
}};
}
@Override
public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
return (Map<Boolean, List<Integer>> acc, Integer candidate) -> {
acc.get(isPrime(acc.get(true),
candidate))
.add(candidate);
};
}
@Override
public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
return (Map<Boolean, List<Integer>> map1, Map<Boolean, List<Integer>> map2) -> {
map1.get(true).addAll(map2.get(true));
map1.get(false).addAll(map2.get(false));
return map1;
};
}
@Override
public Function<Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> finisher() {
return i -> i;
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH));
}
}
참고
- 자바 8 인 액션
- Functional Programming in Java 8
- 모던 자바 (자바8) 못다한 이야기, 케빈 TV
- State of the Lambda
- State of the Lambda: Libraries Edition
- About the Lambda FAQ