ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Java - 스트림(Stream) 최종 처리 메서드의 종류와 사용 방법(1)
    Language/Java 2022. 12. 1. 11:20

    스트림(Stream) 최종 처리 메서드의 종류와 사용 방법(1)

    최종 처리 메서드의 종류

     

    중간 처리 메서드의 리턴 타입은 스트림이었던 반면, 최종 처리 메서드는 기본 타입이거나 OptionalXXX이다. 또한, 소속된 인터페이스가 Stream, IntStream, LongStream, DoubleStream에서 모두 제공된다는 의미이다.

     

    종류로는 매칭, 집계, 루핑, 수집이 있는데 이번 게시글에서는 루핑까지만 살펴보도록 하겠다.

     

    매칭

    매칭은 최종 처리 단계에서 요소들이 특정 조건에 만족하는지 조사할 수 있도록 설계한 것이다. 비슷한 역할로는 중간 처리 단계에서 필터링이 있다.

     

    리턴 타입은 모두 boolean이고, 메서드의 종류로는 allMatch(), anyMatch(), noneMatch()가 있다. 그리고 이 메서드들의 매개 변수는 Predicate를 사용한다. 제공 인터페이스가 Stream일 경우에는 Predicate<T>이고, 나머지에 대해서는 IntPredicate, LongPredicate, DoublePredicate이다.

     

    public class Main {
        public static void main(String[] args) {
            int[] intArr = {2, 4, 6};
    
            boolean result = Arrays.stream(intArr)
                    .allMatch(x -> x % 2 == 0);
            System.out.println("모두 2의 배수인가? : " + result);
    
            result = Arrays.stream(intArr)
                            .anyMatch(x -> x % 3 == 0);
            System.out.println("하나라도 3의 배수가 있는가? : " + result);
    
            result = Arrays.stream(intArr)
                            .noneMatch(x -> x % 3 == 0);
            System.out.println("3의 배수가 없는가? : " + result);
        }
    }

    모든 요소가 2의 배수인지, 하나라도 3의 배수가 존재하는지, 모든 요소가 3의 배수가 아닌지를 조사하는 코드이다.

     

    모두 2의 배수인가? : true
    하나라도 3의 배수가 있는가? : true
    3의 배수가 없는가? : false

    실행 결과는 위와 같다.

     

    기본 집계

    집계는 최종 처리 기능으로 요소들을 처리해서 카운팅, 합계, 평균값, 최댓값, 최솟값 등과 같이 하나의 값으로 산출하는 것을 말한다.

     

     

    위 표를 보면 알 수 있듯이, count(), sum()을 제외하면 리턴 타입이 Optional이라는 것을 알 수 있다. OptionalXXX는 Java 8에서 추가한 java.util 패키지의 새로운 클래스 타입으로, 값을 저장하는 값 기반 클래스들이다. 이 객체에서 값을 얻기 위해서는 get(), getAsDouble(), getAsInt(), getAsLong() 메서드를 호출하면 된다.

     

    public class Main {
        public static void main(String[] args) {
            int[] intArr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
            long count = Arrays.stream(intArr)
                    .filter(x -> x % 2 == 0)
                    .count();
            System.out.println("2의 배수의 개수 : " + count);
    
            long sum = Arrays.stream(intArr)
                    .filter(x -> x % 2 == 0)
                    .sum();
            System.out.println("2의 배수의 합 : " + sum);
    
            double avg = Arrays.stream(intArr)
                    .filter(x -> x % 2 == 0)
                    .average()
                    .getAsDouble();
            System.out.println("2의 배수의 평균 : " + avg);
    
            long max = Arrays.stream(intArr)
                    .max()
                    .getAsInt();
            System.out.println("최댓값 : " + max);
    
            long min = Arrays.stream(intArr)
                    .min()
                    .getAsInt();
            System.out.println("최솟값 : " + min);
    
            long first = Arrays.stream(intArr)
                    .filter(x -> x % 3 == 0)
                    .findFirst()
                    .getAsInt();
            System.out.println("첫 번째 3의 배수 : " + first);
        }
    }

    위 소스 코드는 기본 집계 메서드들을 사용한 예제 코드이다.

     

    2의 배수의 개수 : 5
    2의 배수의 합 : 30
    2의 배수의 평균 : 6.0
    최댓값 : 10
    최솟값 : 1
    첫 번째 3의 배수 : 3

    실행 결과는 위와 같다.

     

    Optional 클래스

     

    위에서 설명했듯이, Optional 클래스는 값을 저장하는 값 기반 클래스이다. 값만 저장하는 것이 아니라, 집계 값이 존재하지 않을 경우 디폴트 값을 설정할 수 있고, 집계 값을 처리하는 Consumer도 등록이 가능하다. 위 표는 Optional 클래스에서 제공하는 메서드 목록이다.

     

    값이 저장되어 있는지 여부가 필요한 경우는 많다. 컬렉션의 요소는 동적으로 추가되는 경우가 많은데, 만약 컬렉션의 요소가 추가되지 않아 저장된 요소가 없는 경우, 평균값을 구하거나 합을 구하는 작업을 수행할 수 없다.

     

    public class Main {
        public static void main(String[] args) {
            List<Integer> list = new ArrayList<>();
    
            double avg = list.stream()
                    .mapToInt(x -> x.intValue())
                    .average()
                    .getAsDouble();
        }
    }

    이러한 예외를 막기 위한 방법은 총 3가지가 있다. 이해를 돕기 위해 아래 코드와 같은 상황을 하나 설정하겠다.

     

    1. Optional 객체를 얻어서 isPresent() 메서드 사용

    public class Main {
        public static void main(String[] args) {
            List<Integer> list = new ArrayList<>();
    
            OptionalDouble optional = list.stream()
                    .mapToInt(x -> x.intValue())
                    .average();
    
            if (optional.isPresent()) {
                System.out.println("평균 : " + optional.getAsDouble());
            } else {
                System.out.println("평균 : 0.0");
            }
        }
    }

    isPresent() 메서드는 값이 저장되어 있는지 확인하는 메서드로, 가장 기본적으로 if문을 활용하여 작성한다.

     

    2. orElse() 메서드로 디폴트 값 설정

    public class Main {
        public static void main(String[] args) {
            List<Integer> list = new ArrayList<>();
    
            double avg = list.stream()
                    .mapToInt(x -> x.intValue())
                    .average()
                    .orElse(0.0);
            System.out.println("평균 : " + avg);
        }
    }

    orElse() 메서드는 값이 저장되어 있지 않을 경우 디폴트 값을 지정하는 역할을 한다. orElse() 메서드를 사용할 경우, 디폴트 값을 지정하는 동시에 값을 반환하기 때문에, getXXX() 메서드를 사용할 필요가 없다.

     

    3. ifPresent() 메서드 사용

    public class Main {
        public static void main(String[] args) {
            List<Integer> list = new ArrayList<>();
    
            list.stream()
                    .mapToInt(x -> x.intValue())
                    .average()
                    .ifPresent(x -> System.out.println("평균 : " + x));
        }
    }

    ifPresent() 메서드는 값이 저장되어 있을 경우 Consumer에서 연산을 수행하도록 한다. 이때, Consumer안에서 람다식으로 하고자 하는 작업을 작성할 수 있다. 위 예제 코드에서는 list에 저장된 값이 없기 때문에, 아무것도 출력되지 않는다.

     

    커스텀 집계

    스트림은 기본 집계 말고도 reduce() 메서드라는 커스텀 집게를 제공한다. 직접 기준을 세워서 집계를 하는 것이다.

     

     

    사용하는 인터페이스는 Stream, IntStream, LongStream, DoubleStream이 있고, 리턴 타입은 Optional 또는 T로 되어 있다. Optional과 T로 리턴되는 기준은 메서드의 매개변수 중 identity의 유무이다. identity 매개변수는 쉽게 말해서 디폴트 값이라고 보면 된다.

     

    public class Main {
        public static void main(String[] args) {
            List<Member> memberList = Arrays.asList(
                    new Member("홍길동", Gender.MALE, 25),
                    new Member("이순신", Gender.MALE, 20),
                    new Member("임꺽정", Gender.MALE, 27)
            );
    
            int sum = memberList.stream()
                    .map(x -> x.getAge())
                    .reduce(Integer::sum)
                    .orElse(0);
            System.out.println(sum);
    
            sum = memberList.stream()
                    .map(x -> x.getAge())
                    .reduce(0, Integer::sum);
            System.out.println(sum);
        }
    }

    위 소스코드에서 첫 번째 부분은 identity 매개 변수를 설정하지 않은 것이고, 두 번째 부분은 설정한 것이다.

    첫 번째 부분은 위의 Optional 클래스에서 설명한 대로 디폴트 값에 대한 예외 처리를 해 주어야 한다. 반면에, 두 번째 부분은 이미 reduce() 메서드에서 디폴트 값을 설정해 주었기 때문에, 리스트에 요소가 없어도 예외가 발생하지 않는다.

     

    reduce() 메서드는 기본적으로 스트림의 요소를 하나씩 방문하면서 작성자가 정한 기준에 따라 누적 연산을 수행한다. 위 예제 코드에서는 덧셈 연산을 설정하였기 때문에 멤버의 나이를 누적 덧셈을 하게 된다.

     

    72
    72

    실행 결과는 위와 같다.

     

    루핑

    루핑은 중간 처리 메서드에서도 사용되고, 최종 처리 메서드에서도 사용된다.

     

    기능 자체는 루핑 한다는 것에서는 동일하지만, 중간 처리를 수행하는 peek() 메서드의 경우  최종 처리 메서드가 없으면 동작하지 않는다. 하지만, forEach() 메서드의 경우 그 자체로 최종 처리 메서드이기 때문에 루핑이 정상적으로 작동한다.

     

    public class Main {
        public static void main(String[] args) {
            int[] intArr = {1, 2, 3, 4, 5};
    
            System.out.println("===== peek() 메서드를 마지막에 호출한 경우 =====");
            Arrays.stream(intArr)
                    .filter(x -> x % 2 == 0)
                    .peek(x -> System.out.println("2의 배수 : " + x));   // 동작하지 않음
    
            System.out.println("===== forEach() 메서드를 마지막에 호출한 경우 =====");
            Arrays.stream(intArr)
                    .filter(x -> x % 2 == 0)
                    .forEach(x -> System.out.println("2의 배수 : " + x));   // 동작함
    
            System.out.println("===== 최종 처리 메서드를 마지막에 호출한 경우 =====");
            Arrays.stream(intArr)
                    .filter(x -> x % 2 == 0)
                    .peek(x -> System.out.println("2의 배수 : " + x))   // 동작함
                    .sum();
        }
    }

    최종 처리 메서드 없이 중간 처리를 하는 peek() 메서드만 호출할 경우 짝수 요소가 출력되지 않는다. 반면에, forEach()는 최종 처리 메서드이기 때문에 짝수 요소를 출력하고 있음을 확인할 수 있다. peek() 메서드를 사용하여 짝수 요소를 출력하기 위해서는 sum() 메서드와 같은 최종 처리 메서드가 존재해야 된다는 것을 확인할 수 있다.

     

    ===== peek() 메서드를 마지막에 호출한 경우 =====
    ===== forEach() 메서드를 마지막에 호출한 경우 =====
    2의 배수 : 2
    2의 배수 : 4
    ===== 최종 처리 메서드를 마지막에 호출한 경우 =====
    2의 배수 : 2
    2의 배수 : 4

    실행결과는 위와 같다.


    출처

    https://steady-coding.tistory.com/314

     

    728x90

    댓글

Designed by Tistory.