Java 8 - Stream

더 자바 Java 8 강의, 모던 자바 인 액션 그리고 자바 공식문서를 보며 Java8 정리

스트림(Stream)

공식문서에서는 스트림을 연속적이고 병렬적인 집계 연산을 지원하는 연속된 요소라 정의했다. 조금 어렵게 느껴지지만 특징을 보면 좀 더 이해하기 쉽다.

  • 연속된 요소: 자료구조인 컬렉션은 요소에 접근하고 저장하는 작업을 주로 하지만, 스트림은 특정 요소 형식으로 이루어진 연속된 값 집합으로 filter, sorted, map 등의 표현 계산을 주로 한다.
  • 데이터 제공 소스: 스트림은 컬렉션, 배열, I/O 등의 자원으로부터 데이터를 소비하는데 이때 스트림은 딱 한 번만 탐색하고 스트림 요소를 모두 소비한다. 기본적으로 함수형의 특징을 가져 원래 데이터를 변경하지 않는다.
  • 파이프라인: 스트림 연산끼리 연결해서 파이프 라인을 구성할 수 있다.
  • 병렬화: 병렬 처리를 쉽게 할 수 있다.

스트림 연산

스트림을 이용하면 filter, map 등 여러 연산 작업을 할 수 있는데 스트림의 연산은 두 가지로 나눌 수 있다.

  • 중간 연산(intermediate operation): 스트림을 연결하여 파이프 라인 구성
    • 다른 스트림 반환
    • lazy: 중간 연산은 최종 연산이 없으면 연산을 수행하지 않는다. 합쳐진 중간 연산을 최종 연산으로 한번에 처리하기 때문이다.
  • 최종 연산(terminal operation): 스트림을 종료
    • 스트림 이외의 결과 반환 (List, Integer, void 등)

중간 연산의 lazy 연산을 확인할 때 가장 쉬운 방법은 출력 구문을 넣어보는 것이다. 첫번째 예제에는 최종 연산없이 중간 연산 filter와 map만 있다. 메인 메서드를 실행해보면 아무 것도 출력되지 않는다.

import java.util.Arrays;
import java.util.List;

public class OpTest1 {
    
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
        
        numbers.stream()
            .filter(n -> {
                System.out.println("filtering: " + n);
                return n % 2 == 0;
            })
            .map(n -> {
                System.out.println("mapping: " + n);
                return n * 2;
            })
    }
}

두번째 예제에는 collect 최종 연산을 추가하여 List로 만들도록 했다. 최종 연산이 이 있으니 중간 연산에 추가한 print문이 실행되었다. 여기서 중간 연산의 동작 방식이 흥미로웠다. filter를 진행하다가 조건에 맞으면 map에 넘어가고 map에서 연산을 수행한 후 다시 filter 연산으로 돌아가는 것을 반복한다.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class OpTest2 {

    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7);

        List<Integer> result = numbers.stream()
                .filter(n -> {
                    System.out.println("(filter) n = " + n);
                    return n % 2 == 0;
                })
                .map(n -> {
                    System.out.println("(map) n = " + n);
                    return n + 1;
                })
                .collect(Collectors.toList());

        System.out.println(result);
		
        /*
        filtering: 1
        filtering: 2
        mapping: 2
        filtering: 3
        filtering: 4
        mapping: 4
        filtering: 5
        filtering: 6
        mapping: 6
        filtering: 7
        [3, 5, 7]                
        */

    }
}

병렬 스트림(parallel stream)

위에서 사용한 stream()이 아닌 parallelStream()을 사용하면 병렬 쓰레드에서 처리할 수 있다. 비용이 드는 부분이 있기 때문에 무조건 빠르다고 판단할 수는 없고 직접 사용해 본 후 쓸지를 판단해야 한다.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class ParallelTest {

    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
        
        numbers.parallelStream()
                .filter(n -> n % 2 == 0)
                .collect(Collectors.toList());
    }

}

주요 Stream API

필터링
  • filter(): Predicate를 인수로 받아 일치하는 모든 요소를 포함하는 스트림 반환
  • distinct(): 고유 요소로 이루어진 스트림 반환
import java.util.Arrays;
import java.util.List;

public class Test {

    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 2, 4, 6, 3, 7, 8, 5);

        numbers.stream()
                .filter(i -> i % 2 == 0)
                .distinct()
                .forEach(System.out::println);
    }
}
매핑
  • map(): 함수를 인수로 받아 함수를 적용한 결과를 새로운 요소로 매핑
  • flatMap(): 스트림의 각 값을 다른 스트림으로 만든 후 모든 스트림을 하나의 스트림으로 연결
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Test {

    public static void main(String[] args) {
        List<String> vegetables = Arrays.asList("carrot", "cabbage", "tomato");

        List<String> collect = vegetables.stream()
                .map(s -> s.split(""))
                .flatMap(Arrays::stream)
                .distinct()
                .collect(Collectors.toList());

        System.out.println("vegetables = " + vegetables);
        System.out.println("collect = " + collect);
        
        // vegetables = [carrot, cabbage, tomato]
		// collect = [c, a, r, o, t, b, g, e, m]

    }
}
매칭
  • allMatch(): 스트림의 모든 요소가 Predicate와 일치하는지 확인
  • anyMatch(): 스트림 요소 중 하나라도 일치하는지 확인
  • noneMatch(): 주어진 Predicate와 일치하는 요소가 없는지 확인
public class Test {

    public static void main(String[] args) {
        List<String> vegetables = Arrays.asList("carrot", "cabbage", "tomato");

        boolean anyMatchResult = vegetables.stream()
                .anyMatch(s -> s.startsWith("c"));

        boolean allMatchResult = vegetables.stream()
                .allMatch(s -> s.startsWith("c"));

        boolean noneMatchResult = vegetables.stream()
                .noneMatch(s -> s.startsWith("c"));


        System.out.println("anyMatchResult = " + anyMatchResult);
        System.out.println("allMatchResult = " + allMatchResult);
        System.out.println("noneMatchResult = " + noneMatchResult);

        // anyMatchResult = true
        // allMatchResult = false
		// noneMatchResult = false

    }
}