Dev

뒤늦게 배워보자, Java 8 Part. 4

August 22, 2018

뒤늦게 배워보자, Java 8 Part. 4

뒤늦게 배워보자, 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));
    }
}

참고