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
'Programming > Java, Spring' 카테고리의 다른 글
자바 함수형 프로그래밍과 디자인 패턴 (0) | 2023.06.21 |
---|---|
자바 함수형 프로그래밍 Scope, Closure&Curry, Lazy Evaluation, Function Composition (0) | 2023.06.20 |
Java Stream, filter, map, sorted, distinct, flatmap (0) | 2023.06.19 |
자바 Gui 프로그래밍 - AWT 클래스 (0) | 2023.06.13 |
자바 멀티 스레드 프로그래밍 (0) | 2023.06.12 |