본문 바로가기
Programming/Java

[자바/Java] 스트림 Stream - 최종 연산

by 코딩하는 랄로 2023. 10. 15.
728x90

https://codingralro.tistory.com/entry/%EC%9E%90%EB%B0%94Java-%EC%8A%A4%ED%8A%B8%EB%A6%BC-Stream-%EC%A4%91%EA%B0%84-%EC%97%B0%EC%82%B0

 

[자바/Java] 스트림 Stream - 중간 연산

https://codingralro.tistory.com/entry/%EC%9E%90%EB%B0%94Java-%EC%8A%A4%ED%8A%B8%EB%A6%BC-Stream-%EC%83%9D%EC%84%B1 [자바/Java] 스트림 Stream - 생성 스트림 (Stream) 이란 자바8에서 등장한 스트림은 람다를 활용할 수 있는 개념

codingralro.tistory.com

저번 글에서는 중간 연산에 대해 공부하였다. 이번 글에서는 스트림의 가장 마지막 단계인 최종 연산에 대해서 알아보겠다.

 

 

최종 연산 - 결과 도출

중간 연산을 통해 가공된 데이터 스트림을 최종적으로 사용할 결과값으로 만드는 단계이다. 그렇기 때문에 스트림을 끝내는 최종 연산이라고 한다.

 

 

Calculating

최대, 최소, 합 평균 등 계산과 관련된 최종 연산을 수행 후 기본형 타입으로 결과를 만들어 낸다.

public class Main {
    public static void main(String[] args) {
    
        //합 - sum() : long을 반환
        long sum = IntStream.of(14, 11, 20, 39, 23).sum();
        
        //갯수 - count() : long을 반환
        long cnt = IntStream.of(14, 11, 20, 39, 23).count();
        
        //최소, 최대 - min(), max()
        //비어 있는 경우, null로 표현 -> Optional 객체를 리턴
        //Optional 메소드를 chaining할 수 있음
        OptioanlInt min = IntStream.of(14, 11, 20, 39, 23).min();
        OptioanlInt max = IntStream.of(14, 11, 20, 39, 23).max();
        
        IntStream.of(14, 11, 20, 39, 23).max().ifPresent(System.out::println); //39
              
    }
}

 

위의 예제에도 나와있듯이, 각각의 메소드가 어떠한 객체를 반환하는지 알고 있는 것이 스트림에서는 매우 중요하다!!

 

 

Reduction

스트림은 reduce라는 메소드를 이용하여 결과를 만들어 낼 수 있다. reduece 메소드는 스트림에서 주요한 메소드 중 하나로 익숙해진다면, 여러 작업을 할 수 있는 메소드이다.

 

reduce는 총 3가지의 파라미터를 받을 수 있다.

  • accumulator : 각 요소를 처리하는 계산 로직, 각 요소가 올 때마다 중간 결과를 생성하는 로직
  • identity : 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴
  • combiner : 병렬(parallel) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작을 하는 로직

 

reduce의 동작 방식은 계산 로직을 통해 나온 결과를 다음 계산 로직의 한 인자로서 넘겨주는 방식으로 동작을 한다.

// 1개 (accumulator)
Optional<T> reduce(BinaryOperator<T> accumulator);

// 2개 (identity)
T reduce(T identity, BinaryOperator<T> accumulator);

// 3개 (combiner)
<U> U reduce(U identity,
  BiFunction<U, ? super T, U> accumulator,
  BinaryOperator<U> combiner);
  
  public class Main {
    public static void main(String[] args) {
    
        //인자가 하나만 있는 경우
        OptionalInt reduced = IntStream.range(1, 5) //[1, 2, 3, 4]
            .redece((a, b) -> Integer.sum(a, b)); // = .reduce(Integer::sum);  
        //과정
        //1 + 2 = 3 -> 다음 연산식으로 넘김
        //3 + 3 = 6 -> 다음 연산식으로 넘김
        //6 + 4 = 10
        
        //인자가 두개 있는 경우
        OptionalInt reducedTwoParams = IntStream.range(1, 5) //[1, 2, 3, 4]
            .redece(10, Integer::sum);
        //과정
        //10 + 1 = 11 -> 다음 연산식으로 넘김
        //11 + 2 = 13 -> 다음 연산식으로 넘김
        //13 + 3 = 16 -> 다음 연산식으로 넘김
        //16 + 4 = 20
        
        //인자가 세개 있는 겨우
        //parallelStream에서만 사용 가능
        Integer reduceParallel = Arrays.asList(1, 2, 3, 4)
            .parallelStream()
            .reduce(10,
                Integer::sum,
                (a, b) -> {
                    System.out.println("combine was called");
                    return a + b;
                });
        //과정
        //10 + 1 , 10 + 2, 10 + 3, 10 + 4 : 동시 진행
        //combine was called
        //11 + 12 = 23 -> 다음 combiner에게 넘김
        //combine was called
        //23 + 13 = 36 -> 다음 combiner에게 넘김
        //combine was called
        //36 + 14 = 50
              
    }
}

 

병렬 스트림이 무조건 시퀀셜보다 좋은 것이 아니다. 간단한 경우에는 오히려 부가처리(combiner)로 인해 더 느린 경우가 있다.

 

 

Collecting

collect 메소드는 또 다른 종료 작업이다. Collector 타입의 인자를 받아서 처리를 하는데, 자주 사용하는 작업은 Collectors 객체에서 제공하고 있다.

 

이번 예제에서는 아래와 같은 간단한 리스트를 사용한다. Product 객체는 수량과 이름을 가지고 있다.

class Product {
    public String name;
    public int amount;
    
    public Product(String name, int amount) {
        this.name = name;
        this.amoount = amount;
    }
    
    public String getName() { return this.name; }
    public int getAmount() { return this.amount; }
}

public class Main {
    public static void main(String[] args) {
        
        List<Product> productList = 
            Arrays.asList(new Product(23, "potatos"),
            Arrays.asList(new Product(14, "orange"),
            Arrays.asList(new Product(13, "lemon"),
            Arrays.asList(new Product(23, "bread"),
            Arrays.asList(new Product(13, "sugar"));
            
        
        //Collectors.toList()
        //스트림에서 작업한 결과를 담은 리스트로 반환
        //각 상품의 이름을 담은 리스트를 반환하는 코드
        List<String> nameList = 
            productList.stream()
                .map(Product::getName)
                .collect(Collectors.toList());
        //[potatos, orange, lemon, bread, sugar]
        
        
        //Collectors.joining
        //스트림에서 작업한 결과를 하나의 문자열로 합칠 수 있다.
        //세 개의 인자를 받을 수 있음
        //delimeter : 각 요소 중간에 들어가 요소를 구분시켜주는 구분자
        //prefix : 결과 맨 앞에 붙는 문자
        //suffix : 결과 맨 뒤에 붙는 문자
        String str = 
            productList.stream()
                .map(Product::getName)
                .collect(Collectors.joining(", ", "<", "]"));
        // <potatos, orange, lemon, bread, sugar]
        
        
        //Collectors.averageingInt()
        //숫자값의 평균을 낸다.
        Double avg = 
            ProductList.stream()
                .collect(Collectors.averagingInt(Product::getAmount));
        //17.2
        
        
        //Collectors.summingInt()
        //숫자값의 합을 낸다.
        Integer sum = 
            ProdunctList.stream()
                .collect(Collectors.summingInt(Product::getAmout));

        //mapToInt 메소드 사용시 더 간단하게 표현 가능
        Integer sum = 
            ProdunctList.stream()
                .mapToInt(Product::getAmount)
                .sum();
         //86
         
         
         //Collectors.summarizingInt()
         //합계, 평균 등 여러 정보를 담은 객체를 리턴
         IntSummaryStatistics statics = 
             ProductList.stream()
                 .collect(Collectors.summarizingInt(Product::getAmount));
         //statics { count = 5, sum = 86, min=13, average=17.200000, max=23}
         //getCount(), getSum(), getAverage(), getMin(), getMax()
         
         
         //Collectors.groupingBy()
         //특정 조건으로 요소들을 그룹지을 수 있다.
         //인자는 함수형 인터페이스인 Function<T, R>이다.
         //수량을 기준으로 그룹핑하는 코드
         Map<Integer, List<Product>> map = 
             productList.stream()
                 .collect(Collectors.groupingBy(Product::getAmount));
         //{23=[Product{amount=23, name='potatoes'}, 
         //     Product{amount=23, name='bread'}], 
         // 13=[Product{amount=13, name='lemon'}, 
         //     Product{amount=13, name='sugar'}], 
         // 14=[Product{amount=14, name='orange'}]}
         
         
         //Collectors.partitioningBy()
         //위의 groupingBy 하수형 인터페이스 Function을 이용하여,
         //특정 값을 기준으로 스트림 내 요소들을 묶었다면,
         //partitioningBy는 함수형 인터페이스 Predicate를 인자로 받는다.
         Map<Boolean, List<Product>> partition = 
             productList.stream()
                 .collect(Collectiors.partiotioningBy(el -> el.getAmount() > 15));
         //{false=[Product{amount=14, name='orange'}, 
         //        Product{amount=13, name='lemon'}, 
         //        Product{amount=13, name='sugar'}], 
         // true=[Product{amount=23, name='potatoes'}, 
         //       Product{amount=23, name='bread'}]}
         
         
         //Collectors.collectiongAndThen()
         //특정 타입으로 결과를 collect한 이후에 추가 작업이 필요한 경우 사용
         public static<T, A, R, RR> Collectior<T, A, RR> collectiongAndThen(
             Collector<T, A, R> downStream,
             Fuction<R, RR> finisher) { ... }
         //finisher는 collect를 한 이후에 실행할 작업을 의미
         //Product를 Set에 저장하고 해당 Set을 수정 불가능한 Set으로 만들어라
         Set<Product> set = 
             productList.stream()
                 .collect(Collectiors.collectingAndThen(Collectors.toSet(),
                                                        Collections::unmodifiableSet);
         
         
         //Collectors.of()
         //직접 collector를 만들 수 있다.
         //accumulator와 combiner는 reduce와 동일
         public static <T, R> Collector<T, R, R> of(
             Supplier<R> supplier,          //new Collector 생성
             BiConsumer<R, T> accumulator,  //두 값을 가지고 계산
             BinaryOperator<R> combiner,    //계산한 결과를 수집하는 함수
             Characteristics... characteristics) { ... }
         //예제
         //collector를 생성
         //supplier -> LinkedList의 생성자
         //accumulator -> 리스트에 추가하는 add 메소드
         //combiner -> 생성된 리스트들을 하나의 리스트로 합치기
         Collector<Product, ?, LinkedList<Product>> toLinkedList = 
             Collector.of(LinkedList::new.
                          LinkedList::add,
                          (first, second) -> {
                              first.addAll(second);
                              return first;
                          });
         LinkedList<Product> list = 
             produnctList.stream()
                 .collect(toLinkedList);
                             
            
    }
}

 

 

Matching

매칭은 조건식 람다 Predicate 를 받아서 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴한다. 다음과 같은 세 가지 메소드가 있다.

  • anyMatch : 하나라도 조건을 만족하는 요소가 있는지
  • allMatch : 모두 조건을 만족하는지
  • noneMatch : 모두 조건을 만족하지 않는지
boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);

public class Main {
    public static void main(String[] args) {
        
        List<String> names = Arrays.asList("Eric", "Elena", "Java");
        
        boolean anyMath = names.stream()
            .anyMatch(name -> name.contains("a"));//true
        boolean allMatch = names.stream()
            .allMatch(name -> name.length() > 4); //false : Elena만 만족
        boolean noneMatch = names.stream()
            .noneMatch(name -> name.endsWith("s"));//true
            
    }
}

 

 

Iterating

forEach는 요소를 돌면서 실행되는 최종 연산이다. 보통 System.out.println 메소드를 통해 결과를 출력할 때 주로 사용된다. peek 메소드는 중간 연산에 해당되고 forEach는 최종 연산에 해당되는 것이 둘의 차이점이다.

public class Main {
    public static void main(String[] args) {
        
        List<String> names = Arrays.asList("Eric", "Elena", "Java");
        
        names.stream().forEach(System.out::println);
        //Eric
        //Elena
        //Java
            
    }
}
728x90