6. 스트림으로 데이터 수집
컬렉터로 스트림의 항목을 컬렉션으로 재구성할 수 있다.
long howManyDishes = menu.stream().collect(Collectors.counting())
다음과 같이 말이다.
물론 이 코드는 menu.stream().count()
로 생략할 수 있다.
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish:;getCalories);
Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy(dishCaloriesComparator));
maxBy
, minBy
를 이용해서 스트림의 최댓값과 최솟값을 계산할 수 있다.summingInt
는 객체를 int로 매핑하는 함수를 인수로 받고 그 값들을 더한다.int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
summingDouble
도 있으며, 합계 뿐만 아니라 평균값 등도 averagingInt
, averagingLong
, averagingDouble
등을 이용해 구할 수 있다.
joining
을 이용해 문자열을 연결할 수도 있다.
List<String> list = List.of("a", "b", "c");
String collect = list.stream().map(String::valueOf)
.collect(Collectors.joining(", "));//delimiter를 넣을 수도 있음
// a, b, c
컬렉터는 reducing 팩토리 메서드로도 정의할 수 있다.
int totalCalories = menu.stream().collect(reducing(0, Dish::getCalories, (i, j) -> i + j));
해당 메서드는 map(Dish::getCalories).reduce(0, (i, j) -> i + j);
로도 요약가능하다.
collect 메서드는 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계된 메서드인 반면 reduce는 두 값을 하나로 도출하는 불변형 연산이라는 점에서 의미론적인 문제가 일어난다. 여러 스레드가 동시에 같은 데이터 구조체를 고치면 리스트 자체가 망가져버리므로 리듀싱 연산을 병렬로 수행할 수 없다는 점도 문제다. 가변 컨테이너 관련 작업이면서 병렬성을 확보하려면 collect 메서드로 리듀싱 연산을 구현하는 것이 바람직하다. 모든 collect가 병렬을 지원하는 것은 아님
팩토리 메서드 Collectors.groupingBy
를 이용하면 쉽게 그룹화할 수 있다.
Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));
{Fish = [params, salmon], OTHER = [french fries, rice, season fruit, pizza]}
분류를 하는 함수를 분류 함수라고 부른다. 분류 함수가 곧 키가 되고, 값은 해당 조율에 포함되는 요소들이다.
Map<Dish.Type, List<Dish>> caloricDishesByType =
menu.stream().filter(dish -> dish.getCalories() > 500)
.collect(groupingBy(Dish::getType));
// {OTHER = [french fries, pizza], MEAT = [pork, beef]}
Map<Dish.Type, List<Dish>> caloricDishesByType =
menu.stream()
.collect(groupingBy(Dish::getType,
filtering(dish -> dish.getCalories() > 500, toList())));
// {FISH = [], OTHER = [french fries, pizza], MEAT = [pork, beef]}
.filter()
로 먼저 거르고 컬렉터를 사용하면 위의 예제와 같이 요소가 없는 Type은 아예 출력이 되질 않는다.
이럴 때에는 groupingBy()
내부에 필터링 컬렉터를 따로 주면된다.
Map<Dish.Type, List<String>> dishNamesByType =
menu.stream()
.collect(groupingBy(Dish::getType, mapping(Dish::getName, toList())));
다음과 같이 밸류를 다른 형태로 매핑할 때에는 mapping
을 사용한다.
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel =
menu.stream().collect(
groupingBy(Dish::getType),
groupingBy(dish -> {
if(dish.getCalories() <= 400) return CaloricLevel.DIET;
if(dish.getCalories() <= 800) return CaloricLevel.NORMAL;
return CaloricLevel.FAT;
}))
);
/*
{MEAT = {DIET = [chicken], NORMAL = [beef], FAT =[pork]},
FISH = {DIET = [prawns] ....}}
다수준 그룹화도 가능하다.
Map<Dish.Type, Long> typesCount = menu.stream().collect(
groupingBy(Dish::getType, counting()));
Map<Dish.Type, Optional<Dish>> mostCaloricByType = menu.stream()
.collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));
두 번째 인수에 원하는 컬렉터를 던져서 수행할 수도 있다.
Map<Dish.Type, Dish> mostCaloricByType = menu.stream()
.collect(groupingBy(Dish::getType,
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)),
Optional::get)));
컬렉터 결과를 다른 형식에 적용할 수도 있다.
- 컬렉터는 점선으로 표시되어 있으며 groupingBy는 가장 바깥쪽에 위치하면서 요리의 종류에 따라 메뉴 스트림을 세 개의 서브스트림으로 그룹화한다.
- groupingBy 컬렉터는 collectingAndThen 컬렉터를 감싼다. 따라서 두 번째 컬렉터는 그룹화된 세 개의 서브스트림에 적용된다.
- collectingAndThen 컬렉터는 세 번째 컬렉터 maxBy를 감싼다.
- 리듀싱 컬렉터가 서브스트림에 연산을 수행한 결과에 collectingAndThen의 Optional::get 변환 함수가 적용된다.
- groupingBy 컬렉터가 반환하는 맵의 분류 키에 대응하는 세 값이 각각의 요리 형식에서 가장 높은 칼로리다.
두 번째 인자로 들어가는 컬렉터가 곧 Map의 value가 된다는 것만 알아두면 이해하기 편하다.
분할은 분할 함수라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능이다. 분할 함수는 불리언을 반환하므로 맵의 키 형식은 Boolean이다. 결과적으로 그룹화 맵은 최대 두 개의 그룹으로 분류된다.
Map<Boolean, List<Dish>> partitionedMenu =
menu.stream().collect(partitioningBy(Dish::isVegetarian));
다음과 같이 말이다.
분할의 장점은 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다는 것이다. 두 번째 인수로 잘게 더 나눌 수도 있다.
{false = {FISH = [prawns, salmon], MEAT = [pork, beef, chicken]},
true = {OTHER = [french fries, rice, season fruit, pizza]}}
partitioningBy(Dish:isVegetarian, collectingAndThen(maxBy(comparingInt(Dish::getCalories)), Optional::get));
이렇게 응용할 수도 있다.
웨지
6장 퀴즈 나갑니다아OX퀴즈 5개 할게용 이유도 달아주심 좋아요
- GroupingBy를 활용하여 PartitionBy와 동일한 분할을 구현할 수 있다 (O / X)
- PartitionBy를 활용하여 GroupingBy와 동일한 그룹핑을 구현할 수 있다 (O / X)
- Collectors의 정적 메소드 counting, summingInt, maxBy, minBy는 요소가 없을 경우 0을 반환한다. (O/X)
- 내 상황에 적합한 Collector 정적 메소드가 없을 경우엔 Collector인터페이스 구현체를 집적 구현하는 방법 밖에 없다. (O/X)
- 병렬 스트림인 경우 Collect도 항상 병렬 리듀싱을 실행한다. (O/X)
답안
O, groupingBy(i -> 조건식) 으로 같은 기능을 구현할 수 있다.
X, partitionBy는 Predecate를 이용한 분류, 따라서 groupingBy와는 같은 구현이 불가능하다.
X, Optional을 반환한다.
X, collect()에서 바로 생성이 가능하다.
X, collect 구현에서 Concurrent 가 되어있어야 수행한다.
손너잘
손너잘은 신규 뱅킹 서비스를 런칭했다. 새로운 서비스에서는, 사용자의 연령대 별 적금에 대한 통계를 보여주고자 한다.
신규 서비스의 개발을 맡은 손너잘은 회사로 부터 아래와 같은 요구사항을 받는다.
public class Main {
static class Customer {
private final String name;
private final int[][] savingLogs;
private final int age;
public Customer(String name, int[][] savingLogs, int age) {
this.name = name;
this.savingLogs = savingLogs;
this.age = age;
}
public String getName() {
return name;
}
public int[][] getSavingLogs() {
return savingLogs;
}
public int getAge() {
return age;
}
}
enum AgeRange {
TEEN_TWENTY,
TWENTY_THIRD,
THIRD_FOURTH;
}
public static void main(String[] args) {
List<Customer> customers = List.of(
new Customer("손너잘", new int[][]{
{74, 80, 1, 68, 74, 80, 59, 68, 74, 80, 59, 68},
{45, 34, 12, 54, 63, 2232, 64, 34, 13, 34, 75, 33},
{23, 34, 66, 624, 74, 43, 1231, 56, 46, 34, 57, 43},
{13, 44, 56, 74, 4235, 45, 6, 23445, 53, 66, 77, 5},
}, 20),
new Customer("손나잘", new int[][]{
{74, 830, 59, 68, 74, 80, 5239, 268, 74, 80, 59, 68},
{45, 34, 22, 54, 63, 2232, 64, 34, 13, 34, 75, 33},
{23, 34, 2366, 64, 74, 43, 13241, 56, 46, 34, 57, 43},
{13, 44, 5623, 723, 45, 45, 643, 54645, 53, 66, 77, 5},
}, 24),
new Customer("발너잘", new int[][]{
{74, 80, 59, 68, 74, 80, 59, 68, 74, 80, 59, 68},
{5, 4, 22, 54, 63, 2, 64, 34, 3, 34, 75, 33},
{23, 3, 6, 4, 74, 43, 11, 56, 6, 34, 7, 43},
{13, 44, 56, 3, 4, 4, 6, 45, 53, 66, 77, 5},
}, 34),
new Customer("발너못", new int[][]{
{74, 80, 59, 68, 74, 80, 59, 68, 74, 80, 59, 68},
{45, 3124, 22, 54, 63, 222, 64, 34, 13, 34, 75, 333},
{23, 3412, 66, 64, 74, 413, 121, 56, 46, 34, 57, 43},
{13, 44, 56, 7, 45, 45, 6, 45, 53, 66, 747, 25},
}, 38),
new Customer("손잘너", new int[][]{
{74, 80, 59, 684, 74, 80, 59, 68, 74, 80, 59, 648},
{45, 334, 22, 54, 63, 22, 64, 34, 13, 334, 75, 133},
{213, 34, 66, 64, 74, 43, 11, 561, 46, 34, 57, 43},
{113, 434, 546, 537, 45, 45, 6, 45, 53, 6136, 77, 5},
}, 18),
new Customer("손너못", new int[][]{
{74, 80, 59, 68, 74, 80, 59, 68, 74, 80, 59, 68},
{42345, 34, 2122, 54, 1363, 22, 64, 34, 13, 34, 75, 33},
{23, 34, 66, 634, 74, 43234, 11, 56, 46, 34, 57, 43435},
{13, 44, 23456, 7, 45, 45, 6, 45, 53, 66, 77, 345},
}, 14),
new Customer("발냄새", new int[][]{
{74, 80, 54329, 68, 74, 523480, 59, 68, 74, 23480, 59, 2348},
{45, 34, 22, 54, 63, 22342, 64, 34, 13, 34, 75, 32343},
{23, 34, 23466, 64, 74, 43, 23411, 56, 4634, 34, 57, 43},
{123453, 44, 56, 7234, 45, 45, 6, 45, 53, 66, 77, 5},
}, 37)
);
}
}
고객들의 4년간의 적금 이력과, 이름, 나이가 주어진다. 주어진 배열의 행은 년수, 열은 각 달마다 저금한 금액을 나타낸다.
적금의 금리는 복리로 계산되며, 매 달마다 이전 달의 금액의 10퍼센트를 더하여 계산한다.
고객의 나이대는 1020, 2030, 30~40 대로 구성되어 있으며 이는 enum 클래스에 지정된 요소만으로 구분해야 한다.
enum클래스는 필요에 따라 언제든지 수정 가능하다.
손너잘이 서비스를 개발할 수 있도록 도와주자. 출력은, 사용자의 연령대별, 이자를 포함한 최고 적금금액과, 이자를 포함한 평균 적금금액을 출력해야 한다.
답안
enum AgeRange {
TEEN_TWENTY(age -> 10 <= age && age < 20),
TWENTY_THIRD(age -> 20 <= age && age < 30),
THIRD_FOURTH(age -> 30 <= age && age < 40);
private final IntPredicate intPredicate;
AgeRange(IntPredicate intPredicate) {
this.intPredicate = intPredicate;
}
public static AgeRange judge(int age) {
return Arrays.stream(values())
.filter(i -> i.intPredicate.test(age))
.findAny()
.get();
}
}
public static void main(String[] args) {
... 중략
Map<AgeRange, DoubleSummaryStatistics> collect = customers.stream()
.collect(
groupingBy(
c -> AgeRange.judge(c.getAge()),
mapping(i -> Arrays.stream(i.getSavingLogs())
.flatMapToInt(Arrays::stream)
.mapToDouble(j -> j)
.reduce(0, (j, k) -> j + k * 1.1),
summarizingDouble(i -> i)
)
)
);
System.out.println(collect.get(AgeRange.TEEN_TWENTY).getMax() + " " + collect.get(AgeRange.TEEN_TWENTY).getAverage());
System.out.println(collect.get(AgeRange.TWENTY_THIRD).getMax() + " " + collect.get(AgeRange.TWENTY_THIRD).getAverage());
System.out.println(collect.get(AgeRange.THIRD_FOURTH).getMax() + " " + collect.get(AgeRange.THIRD_FOURTH).getAverage());
}
파즈
6장
- 적절한 Collectors 클래스의 정적 팩토리 메서드로 답하시오.
- 스트림의 항목에서 정수 프로퍼티 값을 더하기 위한 메서드
- 스트림 항목의 정수 프로퍼티의 평균값을 위한 메서트
- 다른 컬렉터로 감싸고 그 결과에 변환 함수를 적용하기 위한 메서드
- " : " 로 각 String 요소들을 이으려고 한다. collect( ? ) ?에 들어갈 것을 적으시오.
- maxBy와 minBy 는 인자로 어떤 형식을 가지고 있는지 적으시오.
- groupingBy와 partitioningBy는 어떤 점이 다른지 답하시오.
- collect 내의 reducing 메서드와 map 후 reduce 메서드를 하는 것의 차이점을 적으시오.
답안
summingInt(), averagingInt(), collectAndThen()
joining(":")
Comparator
Key 요소가 groupingBy는 K, partitioningBy는 Boolean으로 되어있다.
의미론적인 차이 발생, reduce는 각 요소들을 하나하나 누적시키는 데 목적이 있고 collect는 자료구조를 생성하는데 목적이 있음, 현실적으로는 reduce에서 concurrent하지 않은 자료구조를 생성하고 누적하려고 하면 결과가 박살날수도 있음
라이언
@@@@에 들어갈 코드를 작성하시오,
또한 Function.identity() 를 사용하는것 과 람다식(x->x)을 사용하는 것의 차이를 서술하시오.
public class Application {
public static void main(String[] args)
{
List<String> g
= Arrays.asList("우아한", "테크", "코스");
Map<String, Long> result
= g.stream().collect(
Collectors.@@@@@@(
Function.identity(),
Collectors.@@@@@@()));
System.out.println(result);
}
}
//Output:
// {코스=1, 테크=1, 우아한=1}
답안
toMap, counting(), 본인 자신을 표현한다. 인스터스를 생성하지 않아도 돼서 좋다. 가독성은 그냥 v -> v 이렇게 쓰는 것이 좋다.
완테
6장 질문.
groupingBy 이용하여, 충분히 partitioningBy 를 구현하여 사용할 수 있을 것 같은데요,
그래도 만든 이유는 있을텐데요..
partitioningBy를 이용했을 때 가질 수 있는 장점들이 어떤 것들이 있을까요?
구현 예시
Map<Boolean, List<Employee>> grouped = employees.stream()
.collect(Collectors.groupingBy(Employee::isActive));
List<Employee> activeEmployees = grouped.get(true);
List<Employee> formerEmployees = grouped.get(false)
답안
System.out.println(
Stream.empty().collect(Collectors.partitioningBy(a -> false)));
// Output: {false=[], true=[]}System.out.println(
Stream.empty().collect(Collectors.groupingBy(a -> false)));
// Output: {}
김김
이번 장은 유독 함수형 코드 특유의 괄호가 넘쳐나네요. 코드의 원형이 헷갈려서 공식 docs를 뒤져보니, groupingBy는 다음과 같이 3개의 원형을 가지고 있네요.
groupingBy(Function<? super T,? extends K> classifier)
groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)
groupingBy(Function<? super T,? extends K> classifier, Supplier<M> mapFactory, Collector<? super T,A,D> downstream)
3번은 잘 다루어지지 않고 있으니 생략하겠습니다. 1번과 2번의 원형을 보고 classifier와 downstream으로 받는 것이 무엇인지 생각해보고, 각각의 오버로딩된 함수를 사용했을 때 기대할 수 있는 기댓값(리턴 타입의 차이 등)은 어떻게 다를지 설명해봅시다.
답안
classifier는 Map에서 Key를 매핑하기 위한, 각 요소가 어느 Key에 Mapping 될지 정하는 Function함수
downstream은 value가 어느 형식으로 저장될지 정하는 collector, mapFactory는 어떤 map으로 할지 정해준다고 보면 된다.
중간곰
지난주 완태의 문제 기억하시나요?
병렬 스트림은 실행 순서에 따라 값이 달라질 수 있음을 알게 됐는데요.책을 읽다가 ParallelStream으로도 같은 결과를 만들 수 있다는 사실을 알았어요.
아래 코드를 살짝 고쳐서 아래 기대 결과가 나오게 만들어보세요.
(힌트. 인자 3개짜리 reduce 메서드 이용)
final List<Double> numbers = Arrays.asList(1.0, 2.0, 3.0, 4.0);
final double sequentialMinus = numbers.stream()
.reduce(0.0, (a, b) -> a - b);
final double parallelMinus = numbers.parallelStream()
.reduce(0.0, (a, b) -> a - b);
System.out.println(sequentialMinus + " / " + parallelMinus);
final double sequentialDivide = numbers.stream()
.reduce(1.0, (a, b) -> a / b);
final double parallelDivide = numbers.parallelStream()
.reduce(1.0, (a, b) -> a / b);
System.out.println(sequentialDivide + " / " + parallelDivide);
/*
현재 실행 결과
-10.0 / 0.0
0.041666666666666664 / 1.5
기대 결과
-10.0 / -10.0
0.041666666666666664 / 0.041666666666666664
*/
답안
public static void main(String[] args) {
final List<Double> numbers = Arrays.asList(1.0, 2.0, 3.0, 4.0);
final double sequentialMinus = numbers.stream()
.reduce(0.0, (a, b) -> a - b);
final double parallelMinus = numbers.parallelStream()
.reduce(0.0, (a, b) -> a - b, Double::sum);
System.out.println(sequentialMinus + " / " + parallelMinus);
final double sequentialDivide = numbers.stream()
.reduce(1.0, (a, b) -> a / b);
final double parallelDivide = numbers.parallelStream()
.reduce(1.0, (a, b) -> a / b, (a, b) -> a * b);
System.out.println(sequentialDivide + " / " + parallelDivide);
/*
현재 실행 결과
-10.0 / 0.0
0.041666666666666664 / 1.5
기대 결과
-10.0 / -10.0
0.041666666666666664 / 0.041666666666666664
*/
}
}
검프
Dish의 이름을 의미있게 모으기 위해 사용할 수 있는 기능이 여러개 있습니다.
이번엔 Collectos의 joining, String의 joining, Collectors의 reduce로 해봤는데요.
이 3가지의 차이가 무엇이고, 어떤걸 쓰는게 더 좋을까요?
package modernjavainaction.chap06;
import java.util.List;
import java.util.stream.Collectors;
import static java.util.Arrays.asList;
public class Application {
public static void main(String[] args) {
List<Dish> menu = Dish.menu;
//collectors를 사용한다.
String collectMenu = menu.stream()
.map(Dish::getName)
.collect(Collectors.joining(", "));
System.out.println("collectMenu = " + collectMenu);
//String.join을 사용한다.
String joinMenu = String.join(", ", menu.stream().map(Dish::getName).collect(Collectors.toList()));
System.out.println("joinMenu = " + joinMenu);
//reduce를 사용한다.
String reduceMenu = menu.stream()
.collect(Collectors.reducing("", Dish::getName, (s1, s2) -> s1 + s2 + ", "));
System.out.println("reduceMenu = " + reduceMenu);
}
static public class Dish {
public static final List<Dish> menu = asList(
new Dish("pork"),
new Dish("beef"),
new Dish("chicken"),
new Dish("rice")
);
private final String name;
public Dish(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
}
답안
joining과 String.join은 내부적으로 Stringjoinner를 사용한다. 스트림의 경우 스트림을 여는 오버헤드가 더 있으므로 String.join을 사용한다.
reducing의 경우는 누적연산이 진행될 때 마다 새로운 인스턴스를 생성하므로 성능상 좋지 않다.
'공부 기록들 > 우테코' 카테고리의 다른 글
[모던 자바 인 액션 스터디] 8장 컬렉션 API 개선 (0) | 2021.03.28 |
---|---|
[모던 자바 인 액션 스터디] 7장 병렬 데이터 처리와 성능 (0) | 2021.03.28 |
[모던 자바 인 액션 스터디] 5장 스트림 활용 (0) | 2021.03.14 |
[모던 자바 인 액션 스터디] 4장 스트림 소개 (0) | 2021.03.14 |
순차적 스트림, 병렬 스트림 그리고 findAny와 findFirst 에 대해 (0) | 2021.03.07 |