본문 바로가기
Programming/Java, Spring

자바 Stream, max&min count, match, find, reduce, collectors, to map, grouping by, partitioning by, for each, parallel stream

by Renechoi 2023. 6. 20.

Max and Min Count 

- 스트림 안의 데이터 최대값, 최소값, 개수 

 

- max: Stream 안의 데이터 중 최댁밧을 반환. Stream이 비어있다면 빈 Optional을 반환

- min: Stream 안의 데이터 중 최소값을 반환. Stream이 비어있다면 빈 Optional을 반환

- count: Stream 안의 데이터 개수를 반환 

 

Optional<T> max(Comparator<? super T> comparator);
Optional<T> min(Comparator<? super T> comparator);
long count();

 

스트림 데이터에서 max 값을 추출하기 

Optional<Integer> max1 = Stream.of(5, 3, 6, 2, 1).max((x, y) -> x - y);
Optional<Integer> max2 = Stream.of(5, 3, 6, 2, 1).max(Comparator.comparingInt(x -> x));

 

Integer 클래스의 메서드를 이용해 간단하게 만들 수도 있다. 

Optional<Integer> max3 = Stream.of(5, 3, 6, 2, 1).max(Integer::compareTo);

 

 

min을 이용한 정렬 

 

User user1 = new User().setId(101).setName("Alice").setVerified(true).setEmailAddress("123@123.com");
User user2 = new User().setId(102).setName("Bob").setVerified(false).setEmailAddress("123@123.com");
User user3 = new User().setId(103).setName("Chalie").setVerified(false).setEmailAddress("123@123.com");

User user = Stream.of(user1, user2, user3).min(Comparator.comparing(User::getName)).get();
System.out.println(user);

 

 

양수가 몇개인지 카운트 하기 

 

long count = Stream.of(1, -4, 5, -3, 6).filter(x -> x > 0).count();
System.out.println(count);

 

최근 24시간 이후 가입한 유저들 중에 verified되지 않은 유저를 찾아보자 

 

// 최근 24시간 이후 가입한 유저들 중에서 verified되지 않은 유저 찾기
LocalDateTime now = LocalDateTime.now();
user1.setCreatedAt(now.minusDays(2));
user2.setCreatedAt(now.minusHours(10));
user3.setCreatedAt(now.minusHours(27));

Stream.of(user1,user2,user3)
   .filter(u -> user.getCreatedAt().isAfter(now.minusDays(1)))
   .filter(u ->!user.isVerified())
   .count();

 

 

All Match & Any Match 

 

두 함수 모두 predicate를 받아 boolean을 리턴한다. 

 

- allMatch - Stream 안의 모든 데이터가 predicate를 만족하면 true (전부 &로 엮임)

- anyMatch - Stream 안의 데이터 중 하나라도 predicate를 만족하면 true (전부 or로 엮임) 

 

boolean allMatch(Predicate<? super T> predicate);
boolean anyMatch(Predicate<? super T> predicate);

 

사용 예제 

 

boolean allMatch = Stream.of(3, -4, 1, 0, -5, 3, 10).allMatch(x -> x > 0);

boolean anyMatch = Stream.of(1, 3, -4, 2, 3, 6, 1).anyMatch(x -> x > 0);

 

 

 

Find First & Find Any 

- findFirst - Stream 안의 첫 번째 데이터를 반환. Optional 반환.

- findAny - Stream 안의 아무 데이터나 리턴. 순서가 중요하지 않다. Optional 반환 

 

Stream.of(3,2,-5,6,-7).filter(x-> x< 0).findAny();

Stream.of(-1,-4,6,2,1).filter(x->x>0).findFirst();

 

 

 

 

 

Reduce 

 

- 주어진 함수를 반복 적용해 Stream 안의 데이터를 하나의 값으로 합치는 작업

- 병렬 컴퓨팅에서 자주 등장하는 map reduce와 비슷하다 

- 여러 컴퓨터에서 작업을 하고 하나로 합쳐준다.

- 두 인풋을 받아 하나로 합쳐주는 f(x,y)가 있을 때 -> x, y, z, ... -> f(x,y) -> f(f(x,y), z) -> 

- max, min, count도 결국 reduce의 일종이다. 

 

Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator); 
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

- reduce 1 : 주어진 accumulaotr를 이용해 데이터를 합칩. Stream이 비어있을 경우 Optional 반환. input과 output 타입이 같은 바이너리 오퍼레이터를 stream 안의 데이터에 반복 적용해서 하나의 결과값을 도출해낸다. 

- reduce 2: 반복 작업에 대한 초기값을 줄 수 있다. 주어진 초깃값과 accumulator를 이용. 초깃값이 있기 때문에 항상 반환값이 존재. 

- reduce 3: 합치는 과정에서 타입이 바뀔 경우 사용. Map + reduce로 대체 가능. accumulator는 BiFuntion. -> U 타입의 초기값을 주고 스트림 안의 T 타입의 데이터와 계속 합쳐서 U 타입의 데이터를 만들어내는 것. 

 

실제 코드를 살펴보자. 

 

Reduce를 이용해 integer 합을 구하는 코드 

 

List<Integer> integers = Arrays.asList(1, 4, -2, -5, 3);
int sum = integers.stream().reduce((x, y) -> x + y).get();
System.out.println(sum);

 

reduce 안에서 바이너리 오퍼레이터로 x,y를 받아와 이들의 합을 반환해주는데, 즉, 1 +4 => 5 -2 => 3 -5 => -2 +3 의 과정을 통해 합을 구한다.

 

max나 min을 구하는 로직 

int min = integers.stream().reduce((x, y) ->
   (x > y ? x : y)
).get();

 

 

map 과 reduce를 함께 합치기 : 자주 쓰이는 방법은 아님. 

 

List<String> numbers = Arrays.asList("3", "2", "5", "-4", "1");
int sum2 = numbers.stream()
   .reduce(0, (number, str) -> number + Integer.parseInt(str), (num1, num2) -> num2 + num2);

 

 

 

Collectors 

 

Collectors를 사용해 다양한 컬렉션을 반환할 수 있다. 

 

 

Collectors를 이용한 매핑 

 

Stream.of(3, 5, -3, 3, 4, 5).collect(Collectors.mapping(x-> Math.abs(x), Collectors.toList()));

 

절대값을 적용한 값으로 반환한다. 

 

이것은 결국 map을 먼저하고 collect를 한 것과 동일. 

 

그 외 reduce 등 다른 end function과 연결해서 사용할 수 있다. 

 

 

To map

 

- Stream 안의 데이터를 map 의 형태로 반환해주는 collector

- keyMapper: 데이터를 map의 key로 변환하는 Function

- valueMapper: 데이터를 map의 value로 변환하는 Function 

 

스트림 안의 데이터를 맵으로 바꿔줌으로써 편리하게 사용할 수 있다. 

 

public static <T, K, U> Collector<T, ?, Map<Y,U>> toMap(
  Function<? super T, ? extends K> keyMapper, 
  Function<? super T, ? extends U> valueMaaper)

 

 

keyMapper와 valueMapper 두 함수를 받아와서  key의 리턴타입이 k, value의 리턴타입이 u라면 k와 u에 대한 map을 갖게 된다. 

 

 

 

stream 안에 key mapper와 value mapper가 적용된다. 

 

Stream.of(3, 5, -4, 2, 6).collect(Collectors.toMap())

map 안에 주어야 하는 것으로 키값을 integer를 그대로 가져오는 x 

value에는 의미를 담은 데이터값을 준다. 

 

Stream.of(3, 5, -4, 2, 6).collect(Collectors.toMap(x->x, x->"number is" + x));

 

 

이때

x->x 가 keymapper가 되며

x-> "number is" + x 가 value mapper가 된다. 

 

이렇게 하여 map을 만들게 되면 key는 integer 타입, value는 string 타입 

 

Map<Integer, String> map = Stream.of(3, 5, -4, 2, 6)
   .collect(Collectors.toMap(x -> x, x -> "number is" + x));

System.out.println(map.get(3));

 

 

 

이렇게 map을 만들어주기 때문에 여러 방식으로 활용할 수 있다. 

 

리스트를 받았을 때 이를 id : x  방식의 맵으로 활용하는 것을 보자. 

 


User user1 = new User().setId(101).setName("Alice").setVerified(true).setEmailAddress("123@123.com");
User user2 = new User().setId(102).setName("Bob").setVerified(false).setEmailAddress("123@123.com");
User user3 = new User().setId(103).setName("Chalie").setVerified(false).setEmailAddress("123@123.com");

List<User> users = Arrays.asList(user1, user2, user3);
Map<Integer, User> userMap = users.stream().collect(Collectors.toMap(User::getId, Function.identity()));
System.out.println(userMap);
System.out.println(userMap.get(103));

 

 

 

 

Grouping By 

groupingBy는 classifier라는 함수를 받는다. 이 함수를 적용시켰을 때 같은 데이터끼리 모아서 맵으로 리턴해준다. 

 

이 맵의 key는 classifier의 결과값이고, value는 그 결과값들을 갖는 데이터 리스트다. 

 

예를 들어 stream에 {1,2,3,4,6,8,9}가 있을 때 classifier가 x-> x%3이라면 반환되는 3으로 나눈 나머지인 0, 1, 2중 하나가 된다. 즉 map은 {0 = {3, 6, 9}, 1={1}, 2={2, 8}} 이렇게 페어가 만들어져 매핑이 된다. 

 

groupingBy의 두 번째 파라미터로 또 다른 Collector를 넣을 수 있다. 리스트를 넣을 때 제공된 collector를 적용시킨 값으로 넣게 되는 것이다. 이때 collector의 mapping이나 reducing 등의 함수가 함께 쓰인다. 

 

 

다음과 같은 숫자들이 있을 때

List<Integer> integers = Arrays.asList(12, 2, 101, 203, 304, 402, 305, 349, 2313, 203);

 1의 자리로 묶어서 같은 1의 자리를 갖는 값으로 묶어보자

 

 

Map<Integer, List<Integer>> unitDigitMap = integers.stream().collect(Collectors.groupingBy(number -> number % 10));
System.out.println(unitDigitMap);

 

 

이번에는 두번째 파라미터로 Collector를 준다면 

 

Map<Integer, Set<Integer>> collect = integers.stream()
   .collect(Collectors.groupingBy(number -> number % 10, Collectors.toSet()));

 

Set으로 리턴할 수 있다. 

 

 

다음과 같이 mapping을 이용할 수도 있다. 

 

Map<Integer, List<String>> integerListMap = integers.stream()
   .collect(Collectors.groupingBy(number -> number % 10,
      Collectors.mapping(number -> " unit digit is " + number, Collectors.toList())));

 

grouping을 하여 리스트를 만들어지고, 리스트에 다가 mapping 함수를 적용 시켜서 스트링으로 변환시켜주고 이것을 최종적으로 list의 형태로 반환한다. 

 

 

 

 

orderStatus 별로 order를 묶어보자. => 같은 orderStatus를 묶어서 map을 만들기

 

Map<OrderStatus, BigDecimal> orderStatusToSumOfAmountMap = orders.stream()
   .collect(Collectors.groupingBy(Order::getStatus,
      Collectors.mapping(Order::getAmount,
         Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
System.out.println(orderStatusToSumOfAmountMap);

 

OrderStatus 별로 묶어서 해당 Order들의 값을 합해준다. 

 

groupingBy로 매핑된 결과물에다가 mapping을 해주고, 그 다음에 나온 결과물들에 대해서 reduce하여 하나씩 합쳐준다. 

 

 

 

 

 

 

 

Partitioning By 

 

 

- groupingBy와 유사하지만 Function 대신 Predicate을 받아 true와 false 두 키가 존재하는 map을 반환하는 collector

 

- 마찬가지로 downstream collector를 넘겨 list 이외의 형태로 map의 value를 만드는 것 역시 가능 

 

 

public static <T> Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate)

public static <T, D, A> Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate, Collector<? super T, A, D> downstream)

 

이와 같은 리스트가 있을 때 

List<Integer> integers = Arrays.asList(13, 2, 10, 203, 304, 402, 305, 349, 1234, 302, 332);

 

짝수인 그룹과 홀수인 그룹으로 쪼개보자. 

 

integers.stream().collect(Collectors.partitioningBy(number -> number % ==0))

 

이와 같이 짝수인지를 검증하는 predicate을 넘겨준다. 

 

Map<Boolean, List<Integer>> booleanListMap = integers.stream()
   .collect(Collectors.partitioningBy(number -> number % 2== 0));

System.out.println("even: " + booleanListMap.get(true));
System.out.println("odd: " + booleanListMap.get(false));

 

즉, 어떤 조건을 만족하는 데이터들과 만족하지 않는 데이털, 이렇게 두 그룹으로 분리할 때 사용한다. 

 

 

 

 

유저를 이용한 예제로서 친구들이 많이 있는 유저와 적은 유저들 사이에서, 유저들 중 친구들이 적은 유저들은 "친구들을 만들어라", 많은 친구들은 "놀아보라"라는 이메일을 보내는 로직을 구현해보자. 

 

 

Map<Boolean, List<User>> userPartitions = users.stream()
   .collect(Collectors.partitioningBy(user -> user.getFriendUserIds().size() > 5));

EmailService emailService = new EmailService();

for (User user: userPartitions.get(true)) {
   emailService.sendPlayWithFriendsEmail(user);
}

for (User user: userPartitions.get(false)) {
   emailService.sendMakeMoreFriendsEmail(user);
}

 

5명이 초과인 predicate를 바탕으로 유저 그룹을 두 그룹으로 나누어서 각기 그룹에 대한 다른 로직을 수행할 수 있다. 

 

 

 

 

ForEach 

 

void forEach(Consumer<? super T> action);

 

- consumer를 받아와서 흡수하는 terminator 

- 제공된 ation을 stream의 각 데이터에 적용해준다.

- Java의 iterable 인터페이스에도 forEach가 있기 때문에 Stream의 중간처리가 필요없다면 Iterable collection등에서 바로 쓰는 것도 가능하다. 

 

Arrays.asList(3, 5, 2, 0, 1).stream().forEach(n-> System.out.println(n));
Arrays.asList(3, 5, 2, 0, 1).forEach(System.out::println);

 

 

그렇다면 index를 필요로 하는 for문은 어떻게 해야할까? 

 

-> Instream을 사용한다. 

 

Intstream.range(0, users.size()).forEach(i-> i 처리... );

 

 

 

 

 

Parallel Stream

 

기존의 순차처리하던 stream을 병렬처리가 가능하도록 만들어준다. 

 

- 여러 개의 스레드를 이용하여 stream의 처리 과정을 병렬화

- 중간 과정은 병렬처리 되지만 순서가 있는 stream의 경우 종결 처리 했을 때의 결과물이 기존의 순차적 처리와 일치하도록 종결 처리 과정에서 조정된다. 즉 List로 collect 한다면 순서가 항상 올바르게 나온다는 것. 

 

List<Integer> numbers = Arrays.asList(1, 2, 3);
Stream<Integer> parallelStream = numbers.parallelStream();
Stream<Integer> parallelStream2 = numbers.stream().parallel();

 

병렬을 바로 만들 수도 있고, 기존의 스트림을 병렬로 중간에 바꿀 수도 있다. 

 

 

장점: 

- 굉장히 간단하게 병렬처리를 사용할 수 있게 해준다

- 여러 스레드를 이용해 동시에 중간처리를 하기 때문에 속도가 비약적으로 빨라질 수 있다. 

 

단점:

- 속도가 항상 빨라지는 것은 아니다.

- 처리 과정에서 공통 리소스를 사용할 경우 잘못된 결과가 나오거나 오류가 날 수 있다(deadlock)

- 이를 막기 위해 세마포어를 사용하면 순차처리보다 느려질 수도 있다. 

 

 

 

 

 

검증되지 않은 유저를 골라내서 이메일을 보내는 작업을 순차와 병렬 차이를 비교해보자. 

 

long startTime = System.currentTimeMillis();
users.stream().filter(user -> !user.isVerified())
   .forEach(emailService::sendVerifyYourEmailEmail);
long endTime = System.currentTimeMillis();

System.out.println("순차처리: " + (endTime-startTime) + "ms"); // 5 ~ 9ms



startTime = System.currentTimeMillis();
users.stream().parallel().filter(user -> !user.isVerified())
   .forEach(emailService::sendVerifyYourEmailEmail);
endTime = System.currentTimeMillis();
System.out.println("병렬처리: " + (endTime-startTime) + "ms"); // 3 ~ 6ms

 

 

 

병렬처리시 훨씬더 적은 시간 내에 처리가 가능한 것을 볼 수 있다. 

 

또한 병렬처리시엔 순서가 다르게 섞여 있다. 

 

따라서 중간 처리 과정이 순서에 민감한 작업이라면 parallel stream을 사용할 수 없다. 

 

 

 

 


reference. https://fastcampus.co.kr/dev_red_lsh

 

반응형