이 장의 내용#

  • Collectors클래스로 컬렉션을 만들고 사용하기
    • 하나의 값으로 데이터 스트림 리듀스하기
    • 특별한 리듀싱 요약연산
    • 데이터 그룹화와 분할
    • 자신만의 커스텀 컬렉터 개발
  • 스트림... 중간연산... 최종연산
  • 5장까지는 결과를 toList를 통해 리스트로만 반환을 했다.
  • 6장은 다른 다양한 컬렉션 객체로 반환하는 방법을 본다.
// 명령형 프로그래밍
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();
for (Transaction transaction : transactions) {
    Currency currency = transaction.getCurrency();
    List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency);
    if (transactionsForCurrency == null) {
	    transactionsForCurrency = new ArrayList<>();
	transactionsByCurrencies.put(currency, transactionsForCurrency);
    }
    transactionsForCurrency.add(transaction);
}
 
// 함수형 프로그래밍
// Map<Currency, List<Transaction>> transactionsByCurrencies = 
//	transactions.stream().collect(groupingBy(Transaction::getCurrency));

package lambdasinaction.chap6;

import java.util.*;

public class Dish {

    private final String name;
    private final boolean vegetarian;
    private final int calories;
    private final Type type;

    public Dish(String name, boolean vegetarian, int calories, Type type) {
        this.name = name;
        this.vegetarian = vegetarian;
        this.calories = calories;
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public boolean isVegetarian() {
        return vegetarian;
    }

    public int getCalories() {
        return calories;
    }

    public Type getType() {
        return type;
    }

    public enum Type { MEAT, FISH, OTHER }

    @Override
    public String toString() {
        return name;
    }

    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));
}

6.1 컬렉터란 무엇인가?#

6.1.1 고급리듀싱 기능을 수행하는 컬렉터#

그림 6-1 통화별로 트랜잭션을 그룹화하는 리듀싱 연산
1.PNG
  • 스트림에 collect를 호출하면 스트림의 요소에 리듀싱 연산이 수행된다.

6.1.2 미리 정의된 컬렉터#

  • Collectors가 제공하는 메소드
    • 스트림 요소를 하나의 값으로 리듀스하고 요약
    • 요소 그룹화
    • 요소 분할

6.2 리듀싱과 요약#

  • 컬렉터로 스트림의 모든 항목을 하나의 결과로 합칠수 있다.
import static java.util.stream.Collectors.*;
 
// counting() 팩토리메소드를 사용해서 요리수를 계산
long lowManyDishes = menu.stream().collect(counting());
//long lowManyDishes = menu.stream().count();

6.2.1 스트림값에서 최댓값과 최솟값 검색#

  • Collectors.maxBy, Collectors.minBy 메소드를 사용해서 스트림의 최댓값과 최솟값을 계산할 수 있다.
Comparator<Dish> dishCaloriesComparator = 
    Comparator.comparingInt(Dish::getCalories);

Optional<Dish> mostCaloriesDish = 
    menu.stream().
    collect(maxBy(dishCaloriesComparator));

6.2.2 요약연산#

  • Collectors클래스는 Collectors.summingInt라는 특별한 요약 팩토리 메소드를 제공
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories))
그림 6-2 summingInt 컬렉터의 누적과정
2.PNG

// 평균값 연산의 경우 averagingInt, averagingLong, averagingDouble 메소드를 사용 
double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories));

// 두개 이상의 연산을 한번에 수행. 
// summarizingLong, summarizingDouble메소드가 있고 리턴타입으로는 LongSummaryStatistics, DoubleSummaryStatistics 가 있다. 
IntSummaryStatistics menuStatistics = 
    menu.stream().collect(summarizingInt(Dish::getCalories));
IntSummaryStatistics {
    count = 9, sum = 4300, min = 120, average = 477.77778, max = 800
}

6.2.3 문자열 연결#

// 스트림내 모든 문자열을 합침
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
// 문자열을 합칠때 구분자를 추가
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));

6.2.4 범용 리듀싱 요약 연산#

  • 범용으로 Collectors.reducing 메소드를 제공
// 칼로리 합계 계산
int totalCalories = menu.stream().collect(reducing(
    0, Dish::getCalories, (Integer i, Integer j) -> i + j));
// 가장 칼로리가 높은 음식을 찾는 방법
Optional<Dish> mostCaloriesDish = menu.stream().collect(reducing(
    (d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
  • reducing 메소드의 세개의 인수
    1. 첫번째 : 연산의 시작값
    2. 두번째 : 메뉴의 칼로리 변환 함수
    3. 세번째 : 두 항목을 하나의 값으로 더하는 BinaryOperator
  • collect와 reduce
    • collect 메소드는 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계
    • reduce 메소드는 두 값을 하나로 도출하는 불변형 연산

컬렉션 프레임워크 유연성 : 같은 연산도 다양한 방식으로 수행할 수 있다. #

int totalCalories = menu.stream().collect(reducing(0, // 초깃값
                    Dish::getCalories,	// 변환함수
		    Integer::sum));	// 합계함수
그림 6-3 메뉴의 모든 칼로리를 더하는 리듀싱 과정
3.PNG

public static <T> Collector<T, ?, Long> counting() {
    return reducing(0L, e -> 1L, Long::sum);
}
// 매핑후 리듀싱
int totalCalories = menu.stream().map(Dish::getCalories).reduce(Integer::sum).get();
// IntStream으로 매핑한 후 sum메소드
int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum();

자신의 상황에 맞는 최적의 해법 선택#

  • 스트림에서 제공하는 메소드 사용보다 컬렉터 코드가 더 복잡
  • 재사용성과 커스터마이징이 가능해서 높은 수준의 추상화와 일반화가능

퀴즈 6-1 리듀싱 문자열 연결하기

6.3 그룹화#

// 메뉴를 고기그룹, 생선그룹, 기타그룹으로 그룹화
Map<Dish.Type, List<Dish>> dishesByType = 
    menu.stream().collect(groupingBy(Dish::getType));
// {FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza], MEAT=[pork, beef, chicken]}
  • 분류함수 : 요리에서 Dish.Type과 일치하는 모든 요리를 추출하는 함수를 groupingBy메소드로 전달. 이 함수를 기준으로 스트림이 그룹화
그림 6-4 그룹화로 스트림의 항목을 분류하는 과정
4.PNG

  • 분류기준이 복잡할 경우 메소드 레퍼런스보다는 람다표현식으로 필요한 로직을 처리
public enum CaloricLevel { DIET, NORMAL, FAT };

Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(
	groupingBy(dish -> {
		if (dish.getCalories() <= 400) {
			return CaloricLevel.DIET;
		} else if (dish.getCalories() <= 700) {
			return CaloricLevel.NORMAL;
		} else {
			return CaloricLevel.FAT;
                }
	}));

6.3.1 다수준 그룹화#

Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(
	groupingBy(Dish::getType,	// 첫번째 수준의 분류함수 
		groupingBy(dish -> {	// 두번째 수준의 분류함수
			if (dish.getCalories() <= 400) {
				return CaloricLevel.DIET;
			} else if (dish.getCalories() <= 700) {
				return CaloricLevel.NORMAL;
			} else {
				return CaloricLevel.FAT;
			}
		})
	}));
// {MEAT={DIET=[chichen], NORMAL=[beef], FAT=[pork]},
// FISH={DIET=[prawn], NORMAL=[salmon]},
// OTHER={DIET=[rice, seasonal fruit], NORMAL=[french fries, pizza]}
// }

그림 6-5 n수준 중첩맵과 분류표
5.PNG

6.3.2 서브그룹으로 데이터 수집#

Map<Dish.Type, Long> typesCount = 
	menu.stream().collect(groupingBy(Dish::getType, counting()));
// {MEAT=3, FISH=2, OTHER=4}

Map<Dish.Type, Optional<Dish>> mostCaloricByType = 
	menu.stream().collect(groupingBy(Dish::getType, 
		maxBy(comparingInt(Dish::getCalories))));

// {FISH=Optional[salmon], OTHER=Optional[pizza], MEAT=Optional[pork]}

컬렉터 결과를 다른 형식에 적용하기#

Map<Dish.Type, Dish> mostCaloricByType = menu.stream().collect(
	groupingBy(Dish::getType,	// 분류함수
		collectingAndThen(
			maxBy(comparingInt(Dish::getCalories)),	// 감싸인 컬렉터
		Optional::get)));	// 변환함수
// {FISH=salmon, OTHER=pizza, MEAT=pork}
  • collectingAndThen : 적용할 컬렉터와 변환함수를 인수로 받아 다른 컬렉터를 반환
그림 6-6 여러 컬렉터를 중첩한 효과
6.PNG

  • 컬렉터는 점섬으로 표시되어 있으며 groupingBy는 가장 바깥쪽에 위치하면서 요리의 종류에 따라 메뉴 스트림을 세개의 서브스트림으로 그룹화한다.
  • groupingBy컬렉터는 collectingAndThen 컬렉터를 감싼다. 따라서 두번째 컬렉터는 그룹화된 세개의 서브스트림에 적용된다.
  • collectingAndThen컬렉터는 세번째 컬렉터 maxBy를 감싼다.
  • 리듀싱 컬렉터가 서브스트림에 연산을 수행한 결과에 collectingAndThen의 Optional::get 변환함수가 적용된다.
  • groupingBy컬렉터가 반환하는 맵의 분류키에 대응하는 세 값이 각각의 요리 형식에서 가장 높은 칼로리다.

groupingBy와 함께 사용하는 다른 컬렉터 예제#

  • 스트림에서 같은 그룹으로 분류된 모든 요소에 리듀싱 작업을 수행할때는 팩토리 메소드 groupingBy에 두번째 인수로 전달한 컬렉터를 사용
Map<Dish.Type, Integer> totalCaloriesByType = 
	menu.stream().collect(groupingBy(Dish::getType, 
	summingInt(Dish::getCalories)));

Map<Dish.Type, Set<CaloricLevel>> caloricLevelByType = menu.stream().collect(
	groupingBy(Dish::getType, mapping(dish -> {
		if (dish.getCalories() <= 400) {
			return CaloricLevel.DIET;
		} else if (dish.getCalories() <= 700) {
			return CaloricLevel.NORMAL;
		} else {
			return CaloricLevel.FAT;
                }
	}, toSet())));

Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType = menu.stream().collect(
	groupingBy(Dish::getType, mapping(dish -> { 
		if (dish.getCalories() <= 400) {
			return CaloricLevel.DIET;
		} else if (dish.getCalories() <= 700) {
			return CaloricLevel.NORMAL;
		} else {
			return CaloricLevel.FAT; 
		},
		toCollection(HashSet::new) 
	)));

6.4 분할#

  • 분할 : 분할함수 프레디케이트를 분류함수로 사용하는 특수한 그룹화 기능
  • 분할함수는 불린을 리턴. 맵의 키 타입은 Boolean
Map<Boolean, List<Dish>> partitionedMenu = 
	menu.stream().collect(partitioningBy(Dish::isVegetarian));
// {false=[pork, beef, chicken, prawns, salmon], true=[french fries, rice, season fruit, pizza]}

// 채식요리를 모두 가져온다. 
List<Dish> partitionedDishes = partitionedMenu.get(true);

// 분할함수를 사용하지 않고 필터링을 통해서도 동일한 결과를 얻을수 있다. 
List<Dish> vegetarianDishes = menu.stream().filter(Dish::isVegetarian).collect(toList());

6.4.1 분할의 장점#

  • 분할함수가 반환하는 참,거짓 두가지 요소의 스트림 리스트를 모두 유지하는게 장점
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType = menu.stream().collect(
	partitioningBy(Dish::isVegetarian, 
		groupingBy(Dish::getType)));
// {false={MEAT=[pork, beef, chicken], FISH=[prawns, salmon]}, true={OTHER=[french fries, rice, season fruit, pizza]}}
Map<Boolean, Dish> mostCaloricPartitionedByVegetarian = menu.stream().collect(
	partitioningBy(Dish::isVegetarian,
		collectingAndThen(
			maxBy(comparingInt(Dish::getCalories)),
			Optional::get)));
// {false=pork, true=pizza}
퀴즈 6-2 partitioningBy 사용

6.4.2 숫자를소수와 비소수로 분할하기#

public boolean isPrime(int candidate) {
	return IntStream.range(2, candidate)		// 2부터 candidate미만 사이 자연수를 생성
		.noneMatch(i -> candidate % i == 0);	// 스트림의 모든 정수로 candidate를 나눌수 없으면 true반환
}

// 주어진 수의 제곱근 이하의 수로 제한할수도 있다. 
public boolean isPrime(int candidate) {
	int candidateRoot = (int) Math.sqrt((double) candidate);
	return IntStream.rangeClosed(2, candidateRoot)
		.noneMatch(i -> candidate % i == 0);
}

public Map<Boolean, List<Integer>> partitionPrimes(int n) {
	return IntStream.rangeClosed(2, n).boxed()
		.collect(
			partitioningBy(candidate -> isPrime(candidate)));
}
표 6-1 Collectors클래스의 정적 팩토리 메소드

6.5 Collector 인터페이스#

  • Collector 인터페이스는 리듀싱 연산을 어떻게 구현할지 제공하는 메소드 집합으로 구성
  • Collector인터페이스를 직접 구현해서 컬렉터 생성
public 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은 수집연산 결과객체의 타입

6.5.1 Collector인터페이스의 메소드 살펴보기#

supplier메소드 : 새로운 결과 컨테이너 만들기#

  • 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수
  • ToListCollector에서 supplier 메소드
public Supplier<List<T>> supplier() {
	return () -> new ArrayList<T>();
}

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

  • accumulator 메소드는 리듀싱 연산을 수행하는 함수를 리턴
  • ToListCollector 에서 accumulator가 리턴하는 함수는 이미 탐색한 항목을 포함하는 리스트에 현재 항목을 추가하는 연산 수행
public BiConsumer<List<T>, T> accumulator() {
	return (list, item) -> list.add(item);
//	return List::add;
}

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

  • finisher 메소드는 스트림 탐색을 끝내고 누적자 객체를 최종결과로 변환하면서 누적과정을 끝낼때 호출할 함수를 리턴
  • ToListCollector 처럼 누적자 객체가 이미 최종 결과인 상황도 있어서 이런 경우 항등함수를 리턴
public Function<List<T>, List<T>> finisher() {
	return Function.identity();
}
그림 6-7 순차 리듀싱 과정의 논리적 순서
7.PNG

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

  • combiner 는 스트림의 서로 다른 서브파트를 병령로 처리할때 누적자가 이 결과를 어떻게 처리할지 정의
  • 스트림의 리듀싱을 병렬로 수행가능
public BinaryOperator<List<T>> combiner() {
	return (list1, list2) -> {
		list1.addAll(list2);
		return list1;
	}
}
그림 6-8 병렬화 리듀싱 과정에서 combiner메소드 활용
8.PNG

characteristics 메소드#

  • characteristics 메소드는 컬렉터의 연산을 정의하는 characteristics타입의 불변집합을 리턴
  • characteristics는 스트림을 병렬로 리듀스할지와 병렬로 리듀스한다면 어떤 최적화를 선택할지 힌트 제공하는 이늄
    1. UNORDERED : 리듀싱 결과는 스트림 요소의 순서에 영향을 받지 않음
    2. CONCURRENT : 다중 스레드에서 accumulator함수는 동시에 호출할 수 있으며 스트림의 병렬 리듀싱을 수행가능
    3. IDENTITY_FINISH : 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용가능

6.5.2 응용하기#

import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
import static java.util.stream.Collector.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() {
	// 컬렉터의 플래그를 IDENTITY_FINISH, CONCURRENT로 설정한다. 
        return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT));	
    }
}

6.6 커스텀 컬렉터를 구현해서 성능 개선하기#

public static Map<Boolean, List<Integer>> partitionPrimes(int n) {
	return IntStream.rangeClosed(2, n).boxed()
		.collect(partitioningBy(candidate -> isPrime(candidate)));
}

public static boolean isPrime(int candidate) {
	return IntStream.rangeClosed(2, candidate-1)
		.limit((long) Math.floor(Math.sqrt((double) candidate)) - 1)
		.noneMatch(i -> candidate % i == 0);
}

Add new attachment

Only authorized users are allowed to upload new attachments.

List of attachments

Kind Attachment Name Size Version Date Modified Author Change note
png
1.PNG 665.6 kB 1 27-Oct-2015 21:46 DongGukLee
png
2.PNG 990.9 kB 1 27-Oct-2015 21:46 DongGukLee
png
3.PNG 646.7 kB 1 27-Oct-2015 21:46 DongGukLee
png
4.PNG 554.0 kB 1 27-Oct-2015 21:46 DongGukLee
png
5.PNG 908.1 kB 1 27-Oct-2015 21:46 DongGukLee
png
6.PNG 1,381.2 kB 1 27-Oct-2015 21:46 DongGukLee
png
7.PNG 908.1 kB 1 27-Oct-2015 21:47 DongGukLee
png
8.PNG 1,213.1 kB 1 27-Oct-2015 21:47 DongGukLee
« This page (revision-23) was last changed on 29-Oct-2015 10:13 by DongGukLee