[모던 자바 인 액션] Chapter 6 스트림으로 데이터 수집

OhJuYeong·2025년 12월 22일

모던 자바 인 액션

목록 보기
5/9
post-thumbnail

중간 연산

  • 스트림의 요소를 소비 하지 않음

최종 연산

  • 스트림의 요소를 소비함

컬렉션, 컬렉터, collect이 헷갈리지 않게 주의하기

collect

  • 스트림의 최종 연산 메서드 중 하나

collector

  • Collect에서 필요한 메서드를 정의해놓은 인터페이스

collectors

  • Collector를 구현한 클래스들을 제공

Collectors.toList()

  • Collector를 반환
  • collect(Collectors.toList()) 하면 collect 메서드에 collector 인터페이스가 들어가는 것

Collection

  • Collection Framework에서 최상위 인터페이스

Collection은 Iterable을 상속받는 인터페이스

Collections는 Object를 상속받는 클래스

헷갈릴만두;;;;

6.1 컬렉터란 무엇인가?

함수형 프로그래밍

  • 무엇을 원하는지 직접 명시할 수 있어서 어떤 방법으로 이를 얻을지는 신경 쓸 필요 없음

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

함수형 API 장점

  • 높은 수준의 조합성과 재사용성

컬렉터 장점

  • collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의
  • 스트림에 collect를 호출하면 스트림의 요소에 리듀싱 연산이 수행

collect

  • 리듀싱 연산을 이용해 스트림의 각 요소를 방문하면서 컬렉터가 작업을 처리

6.1.2 미리 정의된 컬렉터

Collectors에서 제공하는 메서드의 기능

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

6.2 리듀싱과 요약

컬렉터

  • 스트림의 모든 항목을 하나의 결과로 합칠 수 있음

ex 1) counting() 이라는 팩토리 메서드가 반환하는 컬렉터로 메뉴에서 요리 수를 계산

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

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

메뉴에서 칼로리 가장 높은 요리 찾기

  • Collectors.maxBy, Collectors.minBy 두개 메서드를 이용해 계산 가능
  • 두 컬렉터 스트림의 요소를 비교하는데 사용할 Comparator를 인수로 받음
Comparator<Dish> dishCaloriesComparator = 
	Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy(dishCaloriesComparator));
  • 스트림에 있는 객체의 숫자 필드의 합계나 평균 등을 반환하는 연산에도 리듀싱 기능이 자주 사용

→ 요약 연산

6.2.2 요약 연산

Collectors 클래스

  • Collectors.summingInt 라는 특별한 요약 팩토리 메서드 제공
  • summingInt
    • 객체를 int로 매핑하는 함수를 인수로 받음
    • 인수로 전달된 함수는 객체를 int로 매핑한 컬렉터를 반환
    • collect 메서드로 전달되면 요약 작업을 수행
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
  • Collectors.summingLong, Collectors.summingDouble 메서드는 같은 방식으로 동작
  • 종종 이들 중 두 개 이상의 연산을 한번에 수행할때 가 있음
  • 이때는 팩토리 메서드 summarizingInt가 반환하는 컬렉터 사용 가능

6.2.3 문자열 연결

컬렉터 joining 팩토리 메서드

  • 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환

메뉴의 모든 요리명을 연결

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

joining 메서드

  • 내부적으로 StringBuilder를 이용해 문자열을 하나로 만듦
  • Dish 클래스가 요리명을 반환하는 toString 메서드를 포함하고 있다면
  • String shortMenu = menu.stream().collect(joining()); 으로 생략 가능

하지만 결과 문자열 해석 불가

  • 두 요소 사이에 구분 문자열 연결 가능
    • 오버로드된 joining 팩토리 메서드
  • String shortMenu = menu.stream().map(Dish::getName).collect(joining(”,”));

6.2.4 범용 리듀싱 요약 연산

모든 컬렉터 reducing 팩토리 메서드로 정의 가능

이거 대신 특화된 컬렉터 사용이유

  • 프로그래밍적 편의성 때문

ex) reducing 메서드로 만들어진 컬렉터로도 모든 메뉴의 칼로리 합계 계산 가능

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

reducing 인수를 세개 받음

  • 첫번째 인수 : 리듀싱 연산의 시작값 이거나 인수가 없을때 반환 값
  • 두번째 인수 : 6.2.2절에서 요리를 칼로리 정수로 변환할 때 사용한 변환 함수
  • 세번째 인수: 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator

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

  • reducing 컬렉터 사용하면 람다 표현식 대신 Integer 클래스의 sum 메서드 참조를 이용하면 코드 단순화 가능
int totalCalories = menu.stream().collect(reducing(0,Dish::getCalories,Integer::sum));

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

  • 하나의 연산을 다양한 방법으로 해결할 수 있음을 보여줌
  • 스트림 인터페이스에서 직접 제공하는 메서드를 이용하는 것에 비해 컬렉터를 이용하는 코드가 더 복잡
  • 복잡한 대신 재사용성과 커스터마이즈 가능성 제공
  • 가장 일반적으로 문제에 특화된 해결책을 고르는 것이 바람직

6.3 그룹화

그룹화

  • 데이터 베이스에서 많이 수행되는 작업
  • 까다롭고 할일이 많고 에러 많이 발생

→ 자바 8의 함수형을 이용하면 가독성 있는 한 줄의 코드로 그룹화 구현 가능

ex) 메뉴를 그룹화 한다고 가정

고기를 포함하는 그룹, 생선을 포함하는 그룹, 나머지 그룹

Collectors.groupingBy를 이용해 쉽게 메뉴 그룹화

Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));
  • 분류 함수

6.3.1 그룹화된 요소 조작

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

ex) 500칼로리 넘는 요리만 필터 가정

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

위 처럼 해결할 수 있지만 FISH가 사라진다는 단점

Map<Dish.Type, List<Dish>> caloricDishesByType = menu.stream()
.collect(groupingBy(Dish::getType,filtering(dish -> dish.getCalories() > 500, toList())));
  • Collector 안으로 필터 프레디케이트를 이동함으로 문제 해결
  • filtering메서드는 Collectors 클래스의 또 다른 정적 팩토리 메서드로 프레디케이트를 인수로 받음
  • 이 프레디케이트로 각 그룹의 요소와 필터링 된 요소를 재그룹화

6.3.2 다수준 그룹화

Collectors.groupingBy를 이용해 항목을 다수준으로 그룹화

  • 일반적인 분류 함수와 컬렉터를 인수로 받음
  • 바깥쪽 groupingBy 메서드에 스트림의 항목을 분류할 두 번째 기준을 정의하는 내부 groupingBy를 전달 해서 두 수준으로 스트림의 항목을 그룹화
Map<Dish.Type, Map<CaloricLevel, List<Dish>> dishesByTypeCaloricLevel = 
menu.stream().collect(groupingBy(Dish::getType, groupingBy(dish -> if(dish.getCalories() <= 400) return CaloircLevel.DIET;
else if(dish.getCalories() <= 700) return CaloricLevel.NORMAL; else return CaloricLevel.FAT;
  • 다수준 그룹화 연산은 다양한 수즌ㅇ로 확장 가능
  • n 수준 그룹 화의 결과는 n수준 트리 구조로 표현되는 n 수준 맵이 됨
  • 보통 groupingBy의 연산을 버킷 개념으로 생각

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

  • 첫번째 groupingBy 로 넘겨주는 컬렉터의 형식은 제한이 없음
  • 분류 함수 한 개의 인수를 갖는 groupingBy(f)는 사실 groupingBy(f,toList()) 의 축약형
Map<Dish.Type, Optional<Dish>> mostCaloricByType = menu.stream().collect(
groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));

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

  • 마지막 그룹화 연산에서 맵의 모든값을 Optional로 감쌀 필요 없으므로 삭제 가능
  • 팩토리 메서드 Collectors.collectingAndThen으로 컬렉터가 반환 결과를 다른 형식으로 활용
Map<Dish.Type, Dish> mostCaloricByType = menu.stream().collect(
groupingBy(Dish::getType, collectingAndThen(maxBy(comparingInt(Dish::getCalories)),Optional::get)));

collectingAndThen

  • 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환
  • 반환되는 컬렉터는 기존 컬렉터의 래퍼 역할을 하며 collect의 마지막 과정에서 변환 함수로 자신의 반환하는 값을 매핑

중첩 컬렉터 작동 가장 외부 계층에서 안쪽으로

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

6.4 분할

분할

  • 분할함수라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능
  • 불리언을 반환하므로 맵의 키 형식은 Boolean
  • 그룹화 맵은 최대 두 개의 그룹으로 분휴

ex) 채식주의자 친구를 저녁에 초대 가정, 모든 요리를 채식 과 채식 아닌 것으로 분류

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

6.4.1 분할의 장점

  • 분할 함수가 반환하는 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다는 것이 분할의 장점
  • 컬렉터를 두번째 인수로 전달할 수 있는 오버로드 된 버전의 partitioningBy 메서드 있음
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType = menu.stream().collect
(partitioningBy(Dish::isVegetarian, groupingBy(Dish::getType)));

6.5 Collector 인터페이스

  • 리듀싱 연산을 어떻게 구현할지 제공하는 메서드 집합으로 구성
  • Collector 인터페이스를 직접 구현해서 더 효율적으로 문제를 해결하는 컬렉터를 만드는 방법 살펴보자!!

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은 수집 연산 결과 객체의 형식

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

  • 위에 네개의 메서드는 collect 메서드에서 실행하는 함수를 반환
  • 다섯 번째 메서드 characteristics는 collect 메서드가 어떤 최적화를 이용해서 리듀싱 연산을 수행할 것인지 결정하도록 돕는 힌트 특성 집합을 제공

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

  • 빈 결과로 이루어진 Supplier 반환
  • supplier는 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수

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

  • 리듀싱 연산을 수행하는 함수를 반환
  • 스트림에서 n 번째 요소를 탐색할 때 두 인수 , 즉 누적자 와 n 번째 요소를 함수에 적용
  • 함수의 반환값은 void
  • 요소를 탐색하면서 적용하는 함수에 의해 누적자 내부 상태가 바뀌므로 누적자가 어떤값일지 단정 못함
  • ToListCollector에서 accumlator가 반환하는 함수는 이미 탐색한 항목을 포함하는 리스트에 현재 항목을 추가하는 연산 수행
public BiConsumer<List<T> ,T> accumulator(){
	return (list, item) -> list.add(item);
}
//메서드 참조로 바꾸면 간결
public BiConsumer<List<T>, T> accumulator(){
	return List::add;
}

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

  • 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환
  • 때로는 ToListCollector에서 볼 수 있는 것 처럼 누적자 객체가 이미 최종 결과인 상황도 있음
  • 이럴때 변환 과정이 필요하지 않으므로 finisher 메서드는 항등 함수를 반환
public Function<List<T> , List<T>> finisher(){
	return Function.identity();
}

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

  • 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이결과를 어떻게 처리할지 정의
  • 스트림의 두 번째 서브파트에서 수집한 항목 리스트를 첫 번째 서브파트 결과 리스트의 뒤에 추가하면 됨
public BinaryOperator<List<T>> combiner(){
	return(list1, list2) -> {
			list1.addAll(list2);
			return list1;
		}
}

Characteristics 메서드

  • 컬렉터의 연산을 정의하는 Characteristics형식의 불변 집합을 반환
  • 스트림을 병렬로 리듀스 할 것인지 그리고 병렬로 리듀스한다면 어떤 최적화를 선택해야할지 힌트 제공
  • UNORDERED
    • 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않음
  • CONCURRENT
    • 다중 스레드에서 accumulator 함수를 동시에 호출 가능
    • 스트림의 병렬 리듀싱을 수행할 수 있음
  • IDENTITY_FINISH
    • 반환하는 함수는 단순히 identity를 적용할 뿐이므로 이를 생략 가능
    • 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용 가능

6.5.2 응용하기

public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
	
	@Override
	public Supplier<List<T>> supplier() {
		return ArrayList::new;
	}

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

	@Override
	public BinaryOperator<List<T>> combiner() {
		return (list1, list2) -> {
				list1.addAll(list2);
				return list1;			
		};
	}
	
	@Override
	public Function<List<T>, List<T>> finisher() {
		return Function.identity();
	}
	
	@Override
	public Set<Characteristics> characteristics() {
		return Collections.unmodifiableSet(EnumSet.of(
				IDENTITY_FINISH, CONCURRENT));
	}
}

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

음.. 이건 알아서 해보는게 .. 맞는 부분인거같음;;

profile
기록하는 개발자

0개의 댓글