Skills/Java

Java - Stream 결과 생성

aoaa 2022. 11. 30. 21:52

 이전 Stream 사용법 포스트에서 내용을 보충하고자 작성한 글입니다.

이전 포스트 끝자락에서도 설명했지만 Filter나 Map 같은 데이터 수정(가공) 즉, 중간 작업만으로는 의미있는 Stream 객체를 만들 수 없기 때문에, 이 가공한 값을 출력하거나 컬렉션으로 모으는 등 마무리 작업이 필요합니다. 이 마무리 작업에 대해 설명해보겠습니다.


1. 결과 생성

1.1 집계(Aggregate)

int sum = IntStream.range(1, 10).sum();
int count = IntStream.range(1, 10).count();
int max = IntStream.range(1, 10).max();
int min = IntStream.range(1, 10).min();
int avg = IntStream.range(1, 10).average();

// 응용 : 짝수의 합
int evenSum = IntStream.range(1, 10)
                       .filter(v -> ((v % 2) == 0))
                       .sum();

 sum(합), count(카운팅), max(최댓값), min(최솟값), average(평균)는 최종 결과를 산출하는 집계 메서드입니다.

이 때, 이 집계 메서드는 return 값으로 Optional 클래스를 return합니다.

public static void main(String[] args){
        int[] intArr = {1, 2, 3, 4, 5};
        
        // average() 요소 평균 반환
        double avg = Arrays.stream(intArr)
                .filter(n -> n%2 == 0)
                .average()
                .getAsDouble();
        System.out.println(avg);

        // max() 최대값 반환
        int max = Arrays.stream(intArr)
                .filter(n -> n%2 == 0)
                .max()
                .getAsInt();
        System.out.println(max);
}

 위와 같이 Optional의 값을 얻기 위해, get(), getAsDouble, getAs() 메서드로 호출이 가능합니다.

Optional 클래스는 단순 집계값 저장 뿐 아니라, 집계값이 존재하지 않을 경우 default 값을 설정할 수 있고(map의 getordefault와 유사), 집계값을 처리하는 Consumer도 등록할 수 있습니다. 코드로 살펴보면

public static void main(String[] args){
        List<Integer> list = new ArrayList<>();

        // isPresent()
        OptionalDouble optional = list.stream()
                .mapToInt(Integer::intValue)
                .average();

        if(optional.isPresent()){
            System.out.println("평균 : " + optional.getAsDouble());
        }else{
            System.out.println("nothing");
        }

        // orElse()
        double avg = list.stream()
                .mapToInt(Integer::intValue)
                .average()
                .orElse(0.0);
        System.out.println("평균 : " + avg);

        // ifPresent()
        list.stream()
            .mapToInt(Integer::intValue)
            .average()
            .ifPresent(data -> System.out.println("평균 : " + data));
}

 list에 아무런 element가 없다면 예외(NoSuchElement)가 발생하는데, 이를 회피하기 위해 

isPresent()는 값이 저장 여부를 판별,

orElse(...)는 값이 저장되어 있지 않을 경우에 default 값을 지정할 수 있으며,

ifPresent(Consumer ...)는 값이 저장되어 있다면, Consumer(예제 같은 경우는 data -> Sout)

 메서드를 통해 처리할 수 있습니다. 


1.2 Reduce

 Reduce는 인자로 BinaryOperator 객체를 받는데, BinaryOperator는 T 타입의 인자 두개를 받고 T 타입의 객체를 리턴하는 함수형 인터페이스입니다.  여기서 연산을 수행하는 부분은 accumulator 함수이며, 직접 구현해서 인자로 전달해야 합니다.

 Stream에서 1부터 10까지 숫자가 전달될 때, 이 값을 모두 합하는 코드를 작성한다고 해보겠습니다.

Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Optional<Integer> sum = numbers.reduce((x, y) -> x + y);
sum.ifPresent(s -> System.out.println("sum: " + s)); // sum: 55

 BinaryOperator는 (total, n) -> total + n와 같은 형식으로 인자가 전달되는데 Stream의 1이 전달될 때, total(0) + n(1) = 1와 같이 계산되고, 여기서 리턴되는 1이 다음에 Stream에서 2가 전달될 때 total로 전달됩니다. 

  • total(0) + n(1) = 1
  • total(1) + n(2) = 3
  • ...
  • total(36) + n(9) = 45
  • total(45) + n(10) = 55

 최종적으로 1부터 10까지의 합 55를 출력하게 됩니다.


1.3 Collect

Collect는 Stream을 List나 Set 등 Collection으로 변경시키는 Stream 메서드입니다. 바로 코드로 살펴보겠습니다.

List<String> alphabet = Arrays.asList("a", "bb", "cc", "bb");

List<String> result = alphabet.stream().collect(Collectors.toList()); // a, bb, ccc, dd
Set<String> result = alphabet.stream().collect(Collectors.toSet()); // a, bb, ccc

먼저 toList, toSet입니다. 모든 Stream의 element를 List 혹은 Set 인스턴스로 변경합니다. 이 때 특정한 Collection으로 변환되는 것이 아니라 더 자세한 Collection을 원한다면 

alphabet.stream().collect(Collectors.toCollection(LinkedList::new));

 toCollection메서드를 이용해 특정 Collection(위 같은 경우 LinkedList)으로 implementation 가능합니다.

 

String result = alphabet.stream().collect(joining()); // abbcccdd

joining()은 List가 아닌 String으로 문자열을 붙여줄 때 사용합니다.


1.4 forEach

 forEach()는 해당 Stream 요소를 하나씩 소모하여 순차적으로 요소에 접근하는 메서드입니다. 이 때 같은 Stream으로는 forEach() 메서드를 한 번밖에 호출할 수 없습니다. (원본 데이터를 소모하는 것은 아니기 때문에, 같은 데이터에서 또 다른 Stream 생성 후 사용은 가능)

Set<Integer> evenNumber = IntStream.range(1, 100).boxed()
                                    .filter(n -> (n%2 == 0))
                                    .forEach(System.out::println);

 1부터 999까지를 filter를 통해 짝수만을 가져오도록 filtering한 뒤, 순차적으로 접근하여 짝수만을 출력하게 됩니다.


2. 마무리

 사실 Stream의 모든 메서드를 설명한 것이 아니라 그저 Stream과 람다식을 공부하고자 작성한 글입니다. Stream에 좀 익숙해지면 API 문서에 자주 사용하는 Stream 메서드를 한 번 정리해보겠습니다. 

 

 

 

 

 

참조