모던자바 인액션 6장

su dong·2023년 8월 29일
0

질문거리

206~207 : 컬렉트와 리듀스 차이


스트림으로 데이터 수집

이 장에서는 collect 역시 다양한 요소 누적 방식을 인수로 받아서 스트림을 최종 결과로 도출하는 리듀싱 연산을 수행할 수 있음을 설명한다.


INTRO

어떤 트랙젝션(거래) 리스트가 있는데 이들을 액면 통화(달러, 원화 등등)로 그룹화한다고 가정하자. 자바 8 람다가 없다면 다음과 같이 작성해야 한다.

Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();

for(Transaction transaction: transcations){
	Currency currency = transaction.getCurrency();
    List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency);
    
    if(transactionsForCurrency == null){
    	transactionsForCurrency = new ArrayList<>();
    	transactionsByCurrencies.put(currency, transactionsForCurrency);
    }
    transactionsForCurrency.add(transaction);
}

위 코드는 익숙하지만 너무 길다. Stream에 toList를 사용하는 대신 더 범용적인 컬렉터 파라미터를 collect 메서드에 전달함으로써 원하는 연산을 간결하게 구현할 수 있다.

Map<Currency, List<Transaction>> transactionsByCurrencies = 
	transactions.stream().collect(groupBy(Transaction::getCurrency));

6.1 컬렉터란 무엇인가?

함수형 프로그래밍에서는 '무엇'을 언하는지 직접 명시할 수 있어서 어떤 방법으로 이를 얻을지는 신경 쓸 필요가 없다.
다수준으로 그룹화 할 때 명령형 프로그래밍과 함수형 프로그래밍의 차이점이 더욱 두드러진다. 명령형 코드에서는 다중 루프와 조건문을 추가하여 가독성 유지보수성이 크게 떨어진다. 반면 함수형 프로그래밍에서는 필요한 컬렉터들을 쉽게 추가할 수 있다.

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

함수형 api의 또 다른 장점은 높은 수준의 조합성과 재사용성이다.
collect에서는 리듀싱 연산을 이요해서 스트림의 각 요소를 방문하면서 컬렉터가 작업을 처리한다.
보통 함수를 요소로 변환할 때는 컬렉터를 적용하여 최종 결과를 저장하는 자료구조에 값을 누적한다.

6.1.2 미리 정의된 컬렉터

6장에서는 Collectors 클래스에서 제공하는 팩토리 메서드의 기능을 설명한다.
기능은 크게 3가지로 분류된다.

  • 스트림 요소를 하나의 값으로 리듀스 하고 요약
  • 요소 그룹화
  • 요소 분할

6.2 리듀싱과 요약

컬렉터로 스트림의 항목을 컬렉션으로 재구성 할 수 있다.

첫번째 예시. counting()이라는 팩토리 메서드가 반환하는 컬렉터로 메뉴에서 요리 수를 계산함.

long howManyDishes = menu.stream().collect(Collectors.counting());

long howManyDishes = menu.stream().count();

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

Collectors.maxBy, Collectors.minBy 두 컬렉터는 스트림의 요소를 비교하는데 사용할 Comparator를 인수로 받는다.

Comparator<Dish> dishCaloriesComparator = 
	Comparator.comparingInt(Dish::getCalories);

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

메뉴가 비어있을 때를 대비해서 Optional< Dish>로 구현한다.


6.2.2 요약 연산

합계나 평균 등을 반환함.
Collectors.summingInt(Long, double)는 객체를 int로 매핑하는 함수를 인수로 받는다. SummingInt의 인수로 전달된 함수는 객체를 int로 매핑한 컬렉터를 반환한다. 그리고 summingInt가 collect 메서드로 전달되면 요약 작업을 수행한다.

int totalCalories = 
	menu.stream().collect(summingInt(Dish::getCalories));

Collectors.averagingInt(Long, double)을 이용해 다양한 형식으로 이루어진 숫자 집합의 평균을 계산할 수 있다.

double avgCalories = 
	menu.stream().collect(averagingInt(Dish::getCalories));

종종 이들 두개 이상의 연산을 한번에 수행해야 할 때도 있다. 이런 상황에서는 팩토리 메서드 summarizingInt가 반환하는 컬렉터를 사용할 수 있다.

IntSummaryStatistics menuStatistics = 
	menu.stream().collect(summarizingint(Dish::getCalories));

반환값 : IntSummaryStatistics{count: 9, sum = 4300, min = 12, average = 477.777878, max = 800}


6.2.3 문자열 연결

컬렉터에 joinig 팩토리 메서드를 이용하면 스트림의 각 객체에 toString메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다.

다음은 메뉴의 모든 요리명을 연결한 코드다.

String shortMenu = 
	menu.stream.map(Dish::getName).collect(joining());

joinig 메서드는 내부적으로 StringBuilder를 이용해서 문자열을 하나로 만든다. Dish 클래스가 요리명을 반환하는 toString 메서드를 포함하고 있다면 다음 코드에서 보여주는 것처럼 map으로 각 요리의 이름을 추출하는 과정을 생략할 수 있다.

String shortMenu = menu.stream().collect(joinig());

구분자를 넣을 수도 있다.

String shortMenu = 
	menu.stream().map(Dish::getName).collect(joining(", "));

반환값: pork, beef, chicken, rice, pizza


6.2.4 범용 리듀싱 요약 연산

지금까지 살펴본 모든 컬렉터는 reducing 팩토리 메서드로도 정의할 수 있다. 그럼에도 불구하고 범용 팩토리 메서드 대신 특화된 컬렉터를 사용한 이유는 프로그래밍적 편의성 때문이다.

예를 들어 다음 코드처럼 reducing메서드로 만들어진 컬렉터로도 메뉴의 모든 칼로리 합계를 계산할 수 있다.

int totalCalories = 
	menu.stream().collect(reducing(0, Dish::getCalories,(i,j)->i+j));

다음처럼 한 개의 인수를 가진 reducing 버전을 이용해서 가장 칼로리가 높은 요리를 찾는 방법도 있다.

Optional<Dish> mostCaloireDish = 
	menu.stream().collect(reducing(
    	(d1, d2) -> d1.getCalories() > d2.getCalories() ? d1: d2));

??collect 와 reduce

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

reducing 컬렉터를 사용한 이전 예제에서 람다 표현식 대신 Integer 클래스의 sum 메서드 참조를 이용하면 코드를 좀 더 단순화할 수 있다.

int totalCalories = 
	menu.stream().collect(reducing(0, Dish::getCalories,Integer::sum);
    
int totalCaloires = 
	menu.stream().maptoInt(Dish::getCalories).sum();

자신에 맞는 최적의 해법 선택하기


6.3 그룹화

데이터 집합을 하나 이상의 특성으로 분류해서 그룹화하는 연산도 데이터베이스에서 많이 수행되는 작업이다.

트랜젝션 통화 그룹화 예제에서 확인했듯이 명령형으로 그룹화를 구현하려면 까다롭고 할일이 많으며, 에러도 많이 발생한다. 하지만 함수형을 이요하면 가독성 있는 한 줄의 코드로 그룹화할 수 있다.

메뉴를 그룹화해보자

Map<Dish.Type, List<Dish>> dishesByType = 
	menu.stream().collect(groupingBy(Dish::getType));

이때 getType과 같이 기준이 되는 함수를 분류함수라고 한다.

단순한 속성 접근자 대신 더 복잡한 분류 기준이 필요한 상황에서는 메서드 참조를 분류 함수로 사용할 수 없다.

예를 들어 400칼로리 이하를 'diet'로, 400~700칼로리를 'normal'로, 700 칼로리 초과를 'fat'요리로 분류한다고 가정하자. Dish 클래스에서는 이러한 연산에 필요한 메서드가 없으므로 메서드 참조를 분류 함수로 사용할 수 없다.

따라서 다음 예제처럼 람다 표현식으로 로직을 구현할 필요가 있다.

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

지금까지는 한가지를 기준으로 그룹화하는 것을 알아봤다.
이제부터는 두가지 기준으로 동시에 그룹화하는 것을 알아보겠다.


6.3.1 그룹화된 요소 조작

요소를 그룹화한 다음에는 각 결과 그룹의 요소를 조작하는 연산이 필요하다.

500칼로리 이상의 요리만 그룹화한다고 생각해보자.

Map<Dish.Type, List<Dish>> caloricDishesByType = 
	menu.stream().filter(dish -> dish.getCalories() >=500)
			    .collect(groupingBy(Dish::getType));

이렇게 되면 문제가 하나 존재하는데

{OTHER = [french fires, pizza], MEAT = [pork, beef]}

위 결과값에서 FISH는 key값 자체가 사라졌다는 것이다.

Collectors 클래스는 일반적인 분류 함수에 Collectors 형식의 두 번째 인수를 갖도록 groupingBy 팩토리 메서드를 오버로드해 이 문제를 해결한다.

다음 코드에서 보여주는 것처럼 두 번째 Collector 안으로 필터 프레디케이트를 이동함으로 이 문제를 해결할 수 있다.

Map<Dish.Type, List<Dish>> caloricDishesByType = 
	menu.stream()
    	.collect(groupingBy(Dish::getType,
        		filtering(dish -> dish.getCalories() >500, toList())));

filtering 메소드는 Collectors 클래스의 또 다른 정적 팩토리 메스드로 프레디케이트를 인수로 받는다. 이 프레디케이트로 각 그룹의 요소와 필터링 된 요소를 재그룹화한다.

{OTHER = [french fires, pizza], MEAT = [pork, beef], FISH=[]}

그룹화된 항목을 조작하는 다른 유용한 기능 중 또 다른 하나로 맵핑 함수를 이용해 요소를 변환하는 작업이 있다. filtering 컬렉터와 같은 이유로 Collectors 클래스는 매핑 함수와 각 항목에 적용한 함수를 모으는데 적용한 함수를 모으는 데 사용하는 또 다른 컬렉터를 인수로 받는 mapping 메서드를 제공한다.

예를 들어 이 함수를 이용해 그룹의 각 요리를 이름 목록으로 변환할 수 있다.

Map<Dish.Type, List<String>> dishNamesByType = 
	menu.stream()
	    .collect(groupingBy(Dish::getType, mapping(Dish::getName, toList())));

6.3.2 다수준 그룹화

두 인수를 받는 팩토리 메서드 Collectors.groupingBy를 이용해서 항목을 다수준으로 그룹화 할 수 있다. Collectors.groupingBy는 일반적으로 분류 함수와 컬렉터를 인수로 받는다. 즉, 바깥쪽 groupingBy 메서드에 스트림의 항목을 분류할 두번째 기준을 정의하는 내부 groupingBy를 전달해서 두 수준으로 스트림의 항목을 그룹화할 수 있다.

Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishsByTypeCaloricLevel = 
	menu.stream().collect(
	    groupingBy(Dish::getType,
          groupingBy(dish-> {
              if(dish.getCalories() <=400) 
                  return CaloricLevel.DIET;
              else if(dish.getCalories()<=700)
                  return CaloricaLevel.NOMAL;
              else return CaloricaLevel.FAT;
          })
    	)
	);

그룹화의 결과로 다음과 같은 두 수준의 맵이 만들어진다.


6.3.3 서브 그룹으로 데이터 수집

첫 번째 groupingBy로 넘겨주는 컬렉터의 형식은 제한이 없다.

Map<Dish.Type, Long> typesCount = menu.stream().collect(
	groupingBy(Dish::getType, count()));

다음은 결과 맵이다.

{MEAT=3. FISH=2, OTHER=4}

분류 함수 한 개의 인수를 갖는 groupingBy(f)는 사실 groupingBy(f, toList())의 축약형이다.

요리의 종류를 분류하는 컬렉터로 메뉴에서 가장 높은 칼로리를 가진 요리를 찾는 프로그램도 다시 구현할 수 있다.

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

-> 결과

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

groupingBy 컬렉터는 스트림의 첫 번째 요소를 찾은 이후에야 그룹화 맵에 새로운 키를 (게으르게)추가한다. 리듀싱 컬렉터가 반환하는 형식을 사용하는 상황이므로 굳이 Optional래퍼를 사용할 필요가 없다.?

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

마지막 그룹화 연산에서 맵의 모든 값을 Optional로 감쌀 필요가 없으므로 Optional을 삭제할 수 있다. 즉, 다음처럼 팩토리 메서드 Collectors.collectingAndThen으로 컬렉터가 반환할 결과를다른 형식으로 활용할 수 있다.

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

collectingAndThen은 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환한다. 반환되는 컬렉터는 기존 컬렉터의 래퍼 역할을 하며 collect의 마지막 과정에서 변환 함수로 자신이 반환하는 값을 매핑한다. 이 예제에서는 maxBy로 만들어진 컬렉터가 감싸지는 컬렉터며 변환 함수 Optional::get으로 반환된 Optional에 포함된 값을 추출한다. 이미 언급했듯이 리듀싱 컬렉터는 절대 Optional.empty()를 반환하지 않으므로 안전한 코드이다.

다음은 맵의 결과다

{FISH=salmon, OTHER=pizza, MEAT=pork}

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

일반적으로 스트림에서 같은 그룹으로 분류된 모든 요소에 리듀싱 작업을 수행할 때는 팩토리메서드 groupingBy를 두 번째 인수로 전달한 컬렉터를 사용한다.

Map<Dish.Type, Integer> totalCaloriesByType = 
	menu.stream().collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));

6.4 분할

분할은 분할함수라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능이다. 분할 함수는 불리언을 반환하므로 맵의 키 형식은 Boolean이다. 결과적으로 그룹화 맵은 최대 두 개의 그룹(참 것짓)으로 분류된다.

예제1) 채식주의자 친구를 위한 채식 인지 아닌지 분류 - partitioningBy

Map<Boolean, List<Dish>> partitionedMenu = 
	menu.stream().collect(partitioningBy(Dish::isVegetarian));

결과1 )

{false=[pork, beef, .. ],, true=[french fries, rice,...]}

이제 참값을 키로 맵에서 모든 채식 요리를 얻을 수 있다.

List<Dish> vegetarianDishes = partitionedMenu.get(true);

물론 메뉴 리스트로 생성한 스트림을 이전 예제에서 사용한 프레디케이트로 필터링한 다음에 별도의 리스트에 결과를 수집해도 같은 결과를 얻을 수 있다.

List<Dish> vegetarianDishes = 
	menu.stream().filter(Dish::isVegetarian).collect(toList());

6.4.1 분할의 장점

분할함수가 반환하는 참, 거짓 두가지 요소의 스트림 리스트를 모두 유지한다는 것이 분할의 장점이다. 채식인 모든 요리 리스트와 채식이 아닌 모든 요리 리스트 둘다를 얻을 수 있다.

또한 다음 코드에서 보여주는 것처럼 컬렉터를 두 번째 인수로 전달할 수 있는 오버로드 된 버전의 partitioningBy 메서드도 있다.

Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType = 
	menu.stream()
    	.collect(partitioningBy(Dish::isVegetarian,
        	groupingBy(Dish::getType)));

위와 같은 결과가 나온다.

다음과 같은 처치도 가능하다.

Map<Boolean, Dish> mostcaloricPartitionedByBegetarian = 
	menu.stream().collect(
    	partitioningBy(Dish::isVegetarian,
        	collectingAndThen(maxBy(comparingInt(Dish::getCalories)),
	            Optional::get))));

다음은 결과다

{false=pork, true=pizza}

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

먼저 소수를 판정하는 isPrime(int candidate)를 만들어준다

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

지금까지 사용한 Collectors 클래스의 정적 팩토리 매서드들!



6.5 Collector 인터페이스

Collector 인터페이스는 리듀싱 연산을 어떻게 구현할지 제공하는 메서드 집합으로 구성된다.
toList()를 통해서 Collector는 어떻게 정의되어 있고, 내부적으로 collect 메서드는 toList가 반환하는 함수를 어떻게 활용했는지 이해할 수 있다.

다음 코드는 Collector 인터페이스의 시그니처와 다섯 개의 메서드 정의를 보여준다.

public interface Collector<T, A, R> {
	 Supplier<A> supplier();
     BiConsumer<A, T> accumulator();
     Function<A, R> finisher();
     BinaryOperator<A> combiner();
     Set<Characteristics> characteristics();
}

위 코드를 다음처럼 설명할 수 있다.

  • T는 수집될 스트림 항목의 제네릭 형식이다.
  • A는 누적자, 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식이다.
  • R은 수집 연산 결과 객체의 형식(항상 그런 것은 아니지만 대개 컬렉션 형식)이다.

예를 들어 Stream< T>의 모든 요소를 List< T>로 수집하는 ToListCollector< T>라는 클래스를 구현할 수 있다.

public class ToListCollector<T> implements Collector<T, List<T>, List<T>>

누적 과정에서 사용되는 객체가 수집 과정의 최종 결과로 사용된다.


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

위 다섯개의 메서드를 하나씩 살펴보자.


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

supplier 메서드는 빈 결과로 이뤄진 Supplier를 반환해야 한다. 즉, supplier는 수집과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수이다.
ToListCollector 처럼 누적자를 반환하는 컬렉터에서는 빈 누적자가 비어있는 스트림의 수집 과정의 결과가 될 수 있다.
ToListCollector에서 supplier는 다음처럼 빈 리스트를 반환한다.

public Supplier<List<T>> supplier(){
	return ()-> new ArrayList<T>();
}

생성자 참조를 전달하는 방법도 있다.

public Supplier<List<T>> supplier(){
	return ArrayList::new;
}

실제 사용 예시

import java.util.List;
import java.util.ArrayList;
import java.util.function.Supplier;

public class SimpleExample {
   public static <T> Supplier<List<T>> supplier() {
       return () -> new ArrayList<T>();
   }

   public static void main(String[] args) {
       Supplier<List<String>> supplier = SimpleExample.supplier();

       List<String> stringList = supplier.get();
       stringList.add("Hello");
       stringList.add("World");

       System.out.println(stringList); // [Hello, World]
   }
}

사용 이유

List stringList = new List<>(); 는 Java에서 실제로 작동하지 않습니다. List는 인터페이스이므로 직접 인스턴스화할 수 없습니다. 대신 ArrayList stringList = new ArrayList<>();와 같은 실제 구현을 인스턴스화해야 합니다.

그러나 여러분의 주요 질문은 왜 Supplier를 사용하여 인스턴스를 생성하는지에 대한 것입니다.

Supplier를 사용하는 것은 여러 시나리오에서 유용할 수 있습니다:

지연 로딩 (Lazy Loading): 객체의 생성을 지연하고 실제 필요할 때만 생성하려는 경우에 Supplier를 사용할 수 있습니다.

팩토리 패턴: 객체의 생성 로직을 중앙화하고 다양한 방법으로 객체를 생성하려는 경우 Supplier를 사용하면 유용합니다.

제네릭 코드: 특정 타입의 객체를 생성하는 로직을 제네릭하게 유지하려는 경우 Supplier를 사용하면 좋습니다. 예를 들어, 특정 컬렉션을 반환하는 메서드가 있고 이 컬렉션의 구현을 바꾸려면 Supplier만 변경하면 됩니다.

재사용성: 같은 로직을 여러 번 재사용해야 하는 경우에 Supplier로 로직을 래핑하여 재사용성을 높일 수 있습니다.

테스트 용이성: 테스트 중에 객체의 생성 방식을 변경하려면 Supplier를 모킹하여 테스트를 쉽게 할 수 있습니다.

다시 말하지만, 모든 상황에서 Supplier를 사용해야 하는 것은 아닙니다. 그러나 특정 상황에서 Supplier의 사용은 코드의 유연성, 재사용성, 그리고 테스트 용이성을 향상시킬 수 있습니다.


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

accumulator 메서드는 리듀싱 연산을 수행하는 함수를 반환합니다.

public BiConsumer<List<T>, T> accumulator(){
	return (list, item) -> list.add(item);
}

다음처럼 메서드 참조를 사용하면 더 깔끔해진다.

public BiConsumer<List<T>, T> accumulator(){
	return List::add;
}

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

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

지금까지 살펴본 세 가지 메스드로도 순차적 스트림 리듀싱 기능을 수행할 수 있다. 실제로는 collect가 동작하기 전에 다른 중간 연산과 파이프라인을 구성할 수 있게 해주는 게으른 특성 그리고 병렬 실행 등도 고려해야 하므로 스트림 리듀싱 기능 구현은 생각보다 복잡하다.


combiner 메서드 : 두 겨로가 컨테이너 병합

리듀싱 연산에서 사용할 함수를 반환하는 메서드. 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의한다.

  • 스트림을 분할해야 하는지 정의하는 조건이 거짓으로 바뀌기 전까지 원래 스트림을 재귀적으로 분할한다.(보통 분산된 작업의 크기가 너무 작아지면 병렬 수행의 속도는 순차수행의 속도보다 느려진다. 즉, 병렬 수행의 효과가 상쇄된다. 일반적으로 프로세싱 코어의 개수를 초과하는 병렬 작업은 효율적이지 않다).

  • 이제 위 그림에서 보여주는 것처럼 모든 서브스트림의 각 요소에 리듀싱 연산을 순차적으로 적용해서 서브스트림을 병렬로 처리할 수 있다.

  • 마지막에는 컬렉터의 cominer 메서드가 반환하는 모든 부분결과를 쌍으로 합친다. 즉, 분할된 모든 서브스트림의 결과를 합치면서 연산이 종료된다.


    Characteristic 메서드

    컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환한다. Characteristic는 스트림을 병렬로 리듀스할 것인지 그리고 병렬로 리듀스한다면 어떤 최적화를 선택해야 할지 힌트를 제공한다.
    Characteristic는 다음 세 항목을 포함하는 열거형이다.

  • UNORERED : 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않는다.

  • CONCURRENT : 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있으며, 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다. 컬렉터의 플래그에 UNORERED를 함께 설정하지 않으면 데이터 소스가 정렬되어 있지 않은(즉, 집합처럼 요소의 순서가 무의미한) 상황에서만 리듀싱을 수행할 수 있다.

  • IDENTITY_FINISH : finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 이를 생략할 수 있다. 따라서 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용할 수 있다. 또한 누적자 A를 결과 R로 안전하게 형변환할 수 있다.


    응용하기 : 나만의 컬렉터 수집기 만들기

    네번째 메서드를 사용하면 스트림의 리듀싱을 병렬로 처리할 수 있다.

profile
사람들을 돕는 문제 해결사, 개발자 sudong입니다. 반갑습니다. tkddlsqkr21@gmail.com

0개의 댓글