본문으로 바로가기

[Java] 자바 8 Stream API 소개 (java.util.stream)

category Backend/Java 2021. 12. 31. 13:35

1. 자바 8 Stream

  • 스트림은 자바 8부터 추가된 컬렉션(배열 포함)의 저장 요소를 하나씩 참조해서 람다식(함수적 스타일)으로 처리할 수 있도록 해주는 반복자입니다.
  • 자바 7 이전까지는 컬렉션을 순차적으로 처리하기 위해 Iterator 반복자를 사용했었습니다.

 

자바 7 이전 Iterator

// 자바 7 이전 Iterator
List<String> list = Arrays.asList("A", "B", "C", "D", "E");

Iterator<String> iterator = list.iterator();

while ((iterator.hasNext())) {
    System.out.println(iterator.next());
}

 

자바 8 이후 Stream

  • Iterator를 사용하는 것보다 훨씬 단순하고 간결하게 처리할 수 있습니다.
  • Stream의 forEach는 함수형 인터페이스인 Consumer를 매개변수로 받고 있어 람다를 활용할 수 있습니다.
  • Stream API는 매개변수로 함수형 인터페이스를 받고, 람다식은 반환 값으로 함수형 인터페이스를 반환합니다.
// 자바 8 Stream
List<String> list = Arrays.asList("A", "B", "C", "D", "E");

list.stream().forEach(System.out::println);
// Stream API
public interface Stream<T> extends BaseStream<T, java.util.stream.Stream<T>> {
    void forEach(Consumer<? super T> action);
    // (. . .) 생략
}

2. Stream의 특징

1. 람다식으로 요소 처리 코드를 제공합니다.

  • Stream이 제공하는 요소 처리 메소드는 함수형 인터페이스 매개 타입을 가지기 때문에 람다식 또는 메소드 레퍼런스를 이용해서 구현할 수 있습니다.
public interface Stream<T> extends BaseStream<T, Stream<T>> {
    Stream<T> filter(Predicate<? super T> predicate);
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    Stream<T> sorted(Comparator<? super T> comparator);
    Stream<T> peek(Consumer<? super T> action);
    void forEach(Consumer<? super T> action);
    // (. . .) 생략 
}

 

2. 내부 반복자를 사용하므로 병렬 처리가 쉽습니다.

[외부 반복자]

  • 개발자가 코드로 직접 컬렉션의 요소를 반복해서 가져오는 코드 패턴
  • Iterator는 요소를 가져오는 것부터 처리하는 것까지 모두 개발자가 작성해야합니다.

[내부 반복자]

  • 컬렉션 내부에서 요소들을 반복시키고, 개발자는 요소당 처리해야 할 코드만 제공하는 코드 패턴
  • 내부 반복자의 장점은 요소의 반복은 컬렉션에게 맡겨두고, 개발자는 요소 처리 코드에만 집중할 수 있습니다. 
  • Stream은 람다식으로 요소 처리 내용만 전달하면 되므로 코드가 간결해집니다.
  • parallelStream을 이용해 병렬 처리를 쉽게 구현할 수 있습니다.

[병렬 처리]

  • 한 가지 작업을 서브 작업으로 나누고, 서브 작업들을 분리된 스레드에서 병렬적으로 처리하는 것
  • 여러 개의 스레드가 요소들을 부분적으로 합하고 이 부분합을 최종 결합해서 전체 합을 생성합니다.  
  • 병렬 처리는 스레드 풀 생성, 스레드 생성 등의 추가적인 비용이 발생하고 자료구조에 따라 성능이 달라질 수 있으므로 순차 처리보다 항상 실행 성능이 좋다고 판단할 수 없습니다.
  • 아래의 실행 결과를 보면 병렬 처리는 여러 스레드를 활용하는 것을 확인할 수 있습니다.
List<String> list = Arrays.asList("A", "B", "C", "D", "E");

// 순차 처리
list.stream().forEach(l ->
        System.out.println(l + " : " + Thread.currentThread().getName())
);

// 병럴 처리
list.parallelStream().forEach(l ->
        System.out.println(l + " : " + Thread.currentThread().getName())
);

순차 처리와 병렬 처리

 

3. Stream은 중간 처리와 최종 처리를 할 수 있습니다. 

  • Stream은 요소에 대해 중간 처리와 최종 처리를 수행할 수 있습니다.
  • 중간 처리(매핑, 필터링, 정렬 등)는 여러 번 사용할 수 있습니다.
  • 최종 처리(반복, 집계처리 등)는 결과 처리이므로 한 번만 사용할 수 있습니다.

EX)  List<People> 컬렉션에서 19세 이상의 사람이 몇 명인지 집계하세요.

Stream 과정

List<People> peopleList = Arrays.asList(
        new People("홍승길", 30),
        new People("김진수", 5),
        new People("이순미", 19),
        new People("김영철", 21),
        new People("박수진", 12)
);

// 19세 이상의 사람이 몇명인지 집계하세요.
long count = peopleList.stream()
        .filter(p -> p.getAge() >= 19)
        .count();
System.out.println(count); // 3

3. Stream의 종류

  • 자바 8부터 새로 추가된 java.util.stream 패키지에는 여러 Stream API들이 포함되어 있습니다.
  • BaseStream 인터페이스를 부모로 해서 실제로 사용하는 자식 인터페이스들이 상속 관계를 이루고 있습니다.
  • Stream은 객체 요소를 처리하는 스트림이고, IntStream, LongStream, DoubleStream은 각각 기본 타입인 int, long, double 요소를 처리하는 스트림입니다.

 

상속 관계

 

Stream 구현 객체를 얻을 수 있는 메소드

Stream 객체 메소드

 

파일로부터 스트림 얻기 예시

Path path = Paths.get("/Users/veneas/Desktop/dev/test.txt");

//Files.lines() 메소드 활용
Stream<String> stream1 = Files.lines(path, Charset.defaultCharset());
stream1.forEach(System.out::println);

//BufferedReader lines() 메소드 활용
File file = path.toFile();
FileReader fileReader = new FileReader(file);
BufferedReader bufferedReader = new BufferedReader(fileReader);
Stream<String> stream2 = bufferedReader.lines();
stream2.forEach(System.out::println);

4. Stream 파이프라인

  • 대량의 데이터를 가공해서 축소하는 것을 리덕션이라 하는데, 데이터의 합계, 평균값, 카운팅, 최댓값, 최솟값 등이 대표적인 리덕션의 결과물이라고 볼 수 있습니다.
  • 컬렉션의 요소를 리덕션의 결과물로 바로 집계할 수 없을 경우에는 중간 처리(필터링, 매핑, 정렬, 그루핑)를 이용하여 가공합니다.
  • Stream은 데이터의 필터링, 매핑, 정렬, 그룹핑 등의 중간 처리와 합계, 평균, 카운팅, 최댓값, 최솟값 등의 최종 처리를 파이프라인으로 해결합니다.
  • 파이프라인은 여러 개의 스트림이 연결되어있는 구조를 말합니다. (마치 컨베이어 벨트에서 분류되는 모습과도 비슷합니다.)
  • 중간 스트림이 생성될 때 요소들이 바로 처리(Eager)되는 것이 아니라 최종 처리가 되기 전까지 중간 처리는 지연(Lazy)됩니다. 

컨베이어 벨트
Stream Pipeline

5. 중간 처리 메소드와 최종 처리 메소드

  • Stream API의 대표적인 메소드입니다. 기본형 Stream 메소드는 생략했습니다.

Stream API 메소드