본문 바로가기
JAVA/[JAVA] Stream

Stream (2)

by oncerun 2022. 2. 15.
반응형

 

Optional과 Java.util.Function 패키지의 인터페이스들, Stream의 생성을 공부했다. 

이를 통해 스트림 초기화 과정과 중간 처리 부분을 배우고

종결처리에 필요한 Optional을 배웠으니 Stream의 종결처리 메서드들을 알아보자.

 

 

1. All Match / Any Match

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

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

 

두 메서드는 인자로 Predicate를 받는다. Predicate는 인자를 받아 boolean을 리턴해주는 함수형 인터페이스이다.

 

allMatch()는 스트림 내부에 있는 모든 데이터가 Predicate를 만족하면 true를 반환한다.

anyMatch()는 스트림 내부에 있는 데이터 중 하나라도 Predicate를 만족하면 true를 반환한다.    

List<Integer> numbers = Arrays.asList(3, -4, 1, 5, 340, 23);

boolean allPositive = numbers.stream()
        .allMatch(number -> number > 0);
log.info("Are all numbers positive :  {}", allPositive);

boolean anyNegative = numbers.stream()
        .anyMatch(number -> number > 0);
log.info("Is any number negative : {}", anyNegative);

 

 

2. Find First / Find Any

Optional<T> findFirst();
Optional<T> findAny();
  • findFirst는 Stream 내부 데이터 중 조건에 일치하는 첫 번째 데이터를 반환하며 Stream이 비어있으면 빈 Optional을 반환합니다.
  • findAny는 Stream 내부 가장 먼저 탐색되는 요소를 리턴합니다. 동일하게 Stream이 비었다면 빈 Optional을 반환합니다.
Optional<Integer> first = Stream.of(1, 2, 65, 6, -1, -3)
        .filter(x -> x <= 0)
        .findFirst();
log.info("first negative {}", first.get());


Optional<Integer> anyPositive = Stream.of(1, 2, 65, 6, -1, -3, 12, 512)
        .filter(x -> x > 0)
        .findAny();
log.info("any findAny {}", anyPositive.get());

Stream을 직렬로 처리할 때 findFirst()와 findAny()는 동일한 요소를 리턴하며, 차이점이 없습니다.

하지만 Stream을 병렬로 처리할 때는 차이가 있습니다.

findFirst()는 여러 요소가 조건에 부합해도 Stream의 순서를 고려하여 가장 앞에 있는 요소를 리턴합니다.

반면에 findAny()는 Multi thread에서 Stream을 처리할 때 가장 먼저 찾은 요소를 리턴합니다. 따라서 Stream의 뒤쪽에 있는 element가 리턴될 수 있습니다.

 

 

3. Reduce

 

Reduce는 다양한 분야에서 사용되는 개념인데 합성함수를 생각하면 이해하기 쉽다. 리듀스는 여러 개의 메서드가 오버 로드되어있습니다.

 

T reduce(T identity, BinaryOperator<T> accumulator);

Optional<T> reduce(BinaryOperator<T> accumulator);

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

 

T reduce(T identity, BinaryOperator <T> accumulator) 

 - BinaryOperator는 BiFuntion을 상속받았지만 2개의 인자와 반환 타입이 모두 T타입인 함수형 인터페이스입니다.  

 - identity를 통해 초기값을 정함으로써 이 값을 통해 시작하게 됩니다. 따라서 항상 리턴 값이 존재합니다.

 

Optional <T> reduce(BinaryOperator <T> accumulator) 

- BinaryOperator를 통해 누산기 연산을 진행합니다. Stream이 비어있다면 빈 Optional을 반환합니다.

 

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

- 초기값과 BiFuntion, BinaryOperator를 가지고 있습니다. 이는 다른 타입과 합치는 과정에 사용되며, 최종 반환 타입은 U타입입니다. BinaryOperator는 병렬 작업이 필요해서 요구됩니다.

 

List<Integer> numbers = Arrays.asList(3, -4, 1, 5, 340, 23);
Optional<Integer> sum = numbers.stream()
        .reduce((x, y) -> x + y);
log.info("sum : {}", sum);

Integer product = numbers.stream()
        .reduce(-1, (x, y) -> x * y);
log.info("product : {}", product);


Integer numberStrList = Arrays.asList("3", "1", "32", "452")
        .stream()
        .map(Integer::parseInt)
        .reduce(0, (x, y) -> x + y);
log.info("numberStrList : {}", numberStrList);

Integer numberStrList2 = Arrays.asList("3", "1", "32", "452")  //type T : String, type U : Integer
        .stream()
        .reduce(0, (number, string) -> number + Integer.parseInt(string), (num1, num2) -> num1 + num2);
log.info("numberStrList2 : {}", numberStrList2);

 

 

4. Collectors

 

스트림의 종결처리로 자바 컬렉션으로 변환할 때. collect(Collectors)를 이용하였습니다..

이 중 자주 사용되는 몇 가지 메서드를 알아보려고 합니다.

public static <T>
Collector<T, ?, List<T>> toList() {
    return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                               (left, right) -> { left.addAll(right); return left; },
                               CH_ID);
}

return타입인 CollectorImple 객체는 다음과 같은 인자를 생성자로 받는다.

Supplier로 새로운 ArrayList를 생성하고,  BiConsumer로 2개의 인자(배열과 배열에 들어갈 요소)를 받아 List에 값을 추가하는 누산기를 하며,  BinaryOperator로 ArrayList를 합쳐서 반환합니다. 리듀싱 작업이 일어나는 것 같습니다. 

 마지막 CH_ID는 확실하게 이해하지는 못했는데 정의된 EumSet의 이름으로 IDENTITY_FINISH라고 정의되어 있네요

List<Integer> numbers = Stream.of(1, 2, 3, 4, 5, 6)
        .collect(Collectors.toList());
System.out.println(numbers);

 

toList와 비슷하게 toSet, toMap도 존재합니다. 

Set<Integer> numberSet = Stream.of(54, 2, 32, 1, 512, 5, 642, 2)
        .collect(Collectors.toSet());
System.out.println(numberSet);

 

스트림에서 데이터를 가공할 때 map연산을 많이 사용하는데, collect내부에서  map연산을 한 후 바로 컬렉션으로 반환하도록 할 수 있습니다.

List<Integer> numberList = Stream.of(123, -2145, -563, 1, 2, 12)
        .collect(Collectors.mapping(x -> Math.abs(x), Collectors.toList()));
System.out.println(numberList);

 

인텔리제이가 map연산을 하도록 권장하는군요

List<Integer> numberList = Stream.of(123, -2145, -563, 1, 2, 12)
.map(Math::abs)
.collect(Collectors.toList());
System.out.println(numberList);

 

reduce연산도 collect안에서 할 수 있습니다. collect() 메서드는  다음과 같이 정의되어 있습니다.

<R> R collect(Supplier<R> supplier,
              BiConsumer<R, ? super T> accumulator,
              BiConsumer<R, R> combiner);
              
              
<R, A> R collect(Collector<? super T, A, R> collector);

인자로 Collector객체를 받을 수 있는데. 이 Collector객체에는 reducing이라는 메서드가 존재합니다.

public static <T> Collector<T, ?, T>
reducing(T identity, BinaryOperator<T> op) {
    return new CollectorImpl<>(
            boxSupplier(identity),
            (a, t) -> { a[0] = op.apply(a[0], t); },
            (a, b) -> { a[0] = op.apply(a[0], b[0]); return a; },
            a -> a[0],
            CH_NOID);
}

 

 

복잡하긴 한데, 첫 번째 static메서드를 보면 초기값, BinaryOperator <T>를 받아 사용자가 지정한 함수를 apply로 실행하여 누산 하고 조합해서 리턴하네요. 

Integer sum = Stream.of(123, -2145, -563, 1, 2, 12)
        .collect(Collectors.reducing(0, (x, y) -> x + y));
System.out.println(sum);

 

5. toMap

 

Map 자료구조는 key-value형식으로 빠르게 데이터를 조회할 수 있는 자료구조입니다.  따라서 스트림을 Map으로 변환하는 작업은 매우 자주 사용됩니다.

( Collector 클래스 안에서 대부분 메서드가 CollectorImpl 객체를 리턴하네요..)

public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper) {
    return new CollectorImpl<>(HashMap::new,
                               uniqKeysMapAccumulator(keyMapper, valueMapper),
                               uniqKeysMapMerger(),
                               CH_ID);
}

 

  • 리턴 타입으로는 Stream 내부 데이터를 Map으로 변환해주는 Collector
  • 첫 번째 인자인 keyMapper는 데이터를 map의 key로 변환하는 Function, 함수형 인터페이스를 받습니다.
  • 두 번째 인자 valueMapper는 데이터를 map의 value로 변환하는 함수형 인터페이스 function을 받습니다.

 

Map<Integer, String> numberMap = Stream.of(3, 1, -2, 33, 6, 8, -569)
        .collect(Collectors
                .toMap(x -> x, y -> "Number is " + y));
                
               // x-> x 대신 Function.identity()사용가능

x -> x를 통해 keyMapper를 전달하고

y -> "Number is " + y로 key에 맵핑되는 valueMapper를 사용하면 

다음과 같은 Map이 만들어집니다.

 

이 상황은 상당히 자주 발생할 것 같다. 왜냐하면 데이터베이스 혹은 외부 API통신 간에서 데이터를 List에 담아 처리하는 경우가 많은데  이 List를 반복을 돌며 어떠한 작업을 해주는 것은 많은 시간이 소요되기 때문에 나는 항상 Map으로 변환하는 작업을 했었던 것 같다. 

Map<Integer, User> userMap = users.stream()
        .collect(Collectors.toMap(User::getId, Function.identity()));
log.info("userMap : {}", userMap);
반응형

'JAVA > [JAVA] Stream' 카테고리의 다른 글

[Java] Functional extends  (0) 2022.02.17
Stream (3)  (0) 2022.02.16
Stream  (0) 2022.02.12
Method Reference  (0) 2022.02.09
Functional Interface (2)  (0) 2022.02.08

댓글