[JAVA] Stream

용용학생·2024년 9월 1일

자바

목록 보기
29/32
post-thumbnail

Stream

Stream이란 사전적 의미로 '물의 흐름'인데, 개발 용어로는 '연속적인 데이터의 흐름'을 의미한다.

JAVA 8 에서 함수형 프로그래밍의 기능을 지원하기 위해 도입되었다.

  • 선언적인 프로그래밍을 통해 더 객체지향적으로 작성이 가능하도록 함

이름이 "A"로 시작되고 길이가 4 이상인 이름을 찾아 정렬하여 출력하는 로직으로 예를 들어본다.

[for-loop로 처리하는 방식]

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

List<String> namesStartedA = new ArrayList<>();
for (String name : names) {
    if (name.startsWith("A") && name.length() >= 4) {
        namesStartedA.add(name);
    }
}

Collections.sort(namesStartedA);

for (String name : namesStartedA) {
    System.out.println(name);
}

[stream으로 처리하는 방식]

names.stream()
    .filter(name -> name.startsWith("A") && name.length() >= 4)
    .sorted()
    .forEach(System.out::println);

두 코드를 보면 알겠지만 stream으로 작성된 코드가 훨씬 간결하고 가독성이 높다.

특징

공식문서에 작성된 특징을 알아보자

  • 저장을 위한 데이터 구조가 아님
    • 컬렉션과는 다르게 스트림은 데이터를 저장하는 목적이 아닌 데이터의 연산을 수행하는 파이프라인의 역할
  • 함수의 특성을 가짐
    • Stream의 연산이 결과는 생성하지만, 원본 소스를 수정하지는 않음
  • Stream은 지연 연산으로 작동됨
  • 스트림 생성 - 중간 연산 - 최종 연산이라는 순서를 가짐
    • 중간 연산은 지연 연산으로 실제 처리가 이루어지지 않고,
      최종 연산에서만 실제 처리가 이루어짐
    • 중간 연산은 Stream 을 반환하고 최종 연산은 Stream 을 소비
  • 유한한 크기를 가지는 Collection 과는 달리 Stream은 무한한 사이즈가 가능

  • Stream은 한 번 생성되고 최종 연산이 되면 닫힘

    • 같은 요소에 대해 처리를 하고싶다면 새로운 Stream을 생성해야 함
  • 중간 연산은 무상태 연산상태 연산으로 나뉨
    • filter()map() 같은 연산은 이전 요소의 정보를 필요로 하지 않으므로 무상태 연산이라 함
    • distinct()sorted()와 같은 연산은 이전 요소의 처리 결과 정보를 가지고 있어야하므로 상태 연산이라 함

for-loop vs stream

그렇다면 언제 for문을 사용하고 언제 stream을 사용할까?

가독성이 더 좋기 때문에 반복이 필요할 때 무조건 stream을 사용해야하나?

하지만 그것은 틀렸습니다.

  • 요소들의 시퀀스에게 일관된 작업을 적용할 때 Stream을 사용하는 게 유리하다.

  • 람다식 내에서는 final 변수(혹은 변경되지 않는 변수)만 읽을 수 있고,
    return 이나 break 가 불가능하다.

  • 또, stream의 목적은 데이터를 가공하기 위한 기능이어서 단순 반복 처리는 for문을 사용하는 게 낫다.

정답은 없으며 취향 차이이다!
팀원 간 혹은 개인 판단 하에 더 나은 것을 적용하자!!

메소드

스트림 생성

배열로 스트림 생성

Arrays.stream() 메서드를 이용해서 생성할 수 있다.

String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);

컬렉션으로 스트림 생성

Collection 인터페이스의 디폴트 메소드인 stream을 이용해서 생성할 수 있다.

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();

기본 타입 스트림

Primitive Type의 스트림을 기본적으로 제공한다.

IntStream intStream = IntStream.range(0, 5); // [0, 1, 2, 3, 4]
DoubleStream doubleStream = DoubleStream.of(1.0, 2.5, 4.0);

Stream.generate()

static <T> Stream<T> generate(Supplier<T> s) { ... }

Supplier<T>로 스트림을 생성할 수 있다.

다만 주의해야할 점은 Infinite Stream이 생성되기 때문에 크기를 제한하는 limit() 같은 short-circuit의 기능이 있는 연산을 꼭 사용해야 한다.

Stream.iterate()

static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
static <T> Stream<T> iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next)

초기값과 그 값을 인자로 하는 람다식을 이용해서 스트림을 만든다.

이 역시 Infinite Stream을 생성하기 때문에 사이즈 제한을 해야 한다.

중간 연산

중간 연산은 스트림을 리턴하기 때문에 메서드 체이닝으로 이어 작성할 수 있다.

map()

<R> Stream<R> map(Function<? super T, ? extends R> mapper)

스트림 내 요소들을 mapper 인자로 받은 연산을 통해 특정 값들로 변환하여 새로운 스트림을 만든다.

int[] arr = new int[]{1, 2, 3, 4};

Arrays.stream(arr)
    .map(it -> it * 10); // [10, 20, 30, 40]

flatMap()

<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)

map() 메서드와 비슷하지만 다른 점은 리턴 값이 Stream인 Fuction 함수를 인자로 넘겨야 합니다.

이 메서드는 중첩 구조를 한 단계 벗겨내서 flat하게 만들어 반환해줍니다.

String[] str = {"Hello", "World"};

Arrays.stream(str)
    .map(it -> it.split("")
    .flatMap(Arrays::stream); // [H, e, l, l, o, W, o, r, l, d]

filter()

Stream<T> filter(Predicate<? super T> predicate)

스트림 내 요소들을 순회하며 predicate 인자로 들어온 연산의 결과가 true 인 요소들만 있는 스트림을 반환한다.

IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    .filter(it -> it % 2 == 0); // [2, 4, 6, 8, 10]

sort()

Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)

인자 없는 메서드 호출 시 기본적으로 오름차순으로 정렬한다.

최종 연산

가공한 스트림을 끝내서 결과를 반환하는 메서드들로 이루어져 있다.

for-loop와의 성능 차이

기본적으로 stream보다 for-loop의 성능이 더 좋다고 한다.

Effective Java의 공저자인 Langer의 컨퍼런스에서 for-loop와 stream 성능을 비교한 자료가 있다.

이 내용에 대해서 정리가 잘 된 블로그가 있어 참조한다.

profile
자바 스프링 공부하는 정리 블로그!

0개의 댓글