Stream에 대해 알아보자

박대화·2023년 11월 29일

알아봅시다

목록 보기
2/2

Stream

스트림은 Java 8에 추가되어 람다를 통해 컬렉션을 다루는 기능입니다.
스트림을 사용하지 않는다면 for, foreach 문을 사용해서 컬렉션의 요소를 하나하나 처리해야 합니다.
간단한 로직에서는 크게 상관없지만, 복잡한 로직에서는 코드의 양이 많아져 로직이 섞이게 되어 코드를 알아보기 쉽지 않습니다.

스트림은 데이터의 흐름으로 정의할 수 있습니다.
스트림은 가공한 결과가 스트림으로 반환되어 파이프라인으로 '.'으로 이어서 다른 작업이 가능합니다.

또한 스트림은 병럴처리가 간단하기 때문에 많은 요소들을 스레드를 이용해 빠르게 처리할 수 있습니다.

스트림의 구조는 3가지로 이루어져 있습니다.
1. 생성 : 스트림 인스턴스 생성
2. 가공 : 필터링 및 매핑으로 데이터를 가공
3. 결과 : 원하는 출력값으로 결과 생성

스트림 생성

다양한 방법으로 스트림을 생성할 수 있습니다.

import java.util.stream.*;

스트림은 stream을 임포트해야 사용할 수 있습니다.

배열 스트림

String[] arr = new String[]{"a", "b", "c"};

Stream<String> stream = Arrays.stream(arr);
stream.forEach(s -> System.out.print(s + " "));
// a b c

System.out.println();
Stream<String> subStream = Arrays.stream(arr, 1, 3);
subStream.forEach(s -> System.out.print(s+" "));

Arrays.asList(arr).stream().forEach(System.out::print);
// b c

배열을 Arrays.stream() 함수 자체로 Stream으로 변환해줄 수도 있고,
Arrays.asList()로 리스트로 변환해준 후에 Stream으로 변환해줘도 됩니다.

기본 자료형 배열 스트림

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

Arrays.stream(nums).boxed().forEach(System.out::print);
// 123

int, float, double과 같은 기본 자료형으로 만들어진 배열은 stream에서 자동으로 박싱이 되지 않기 때문에 boxed() 함수를 사용해 참조형으로 변환해야 합니다.

컬렉션 스트림

List<String> str = new ArrayList<>();
str.add("a");
str.add("b");
str.add("c");

str.stream().forEach(System.out::print);
// abc

System.out.println();
Set<String> set = new HashSet<>(str);
set.stream().forEach(System.out::print);
// abc

컬렉션 타입의 경우 .stream()을 통해 스트림으로 변환이 가능합니다.

비어 있는 스트림

Stream.empty();

빈 스트림은 요소가 없을 때 null 대신 사용합니다.

Stream.builder()

Stream.builder()
       .add("a")
       .add("b")
       .add("c")
       .build()
       .forEach(System.out::print);
 // abc

Builder 패턴으로 직접 값을 넣어줄 수 있습니다.

Stream.generate()

Stream.generate(() -> "a").limit(3).forEach(System.out::print);
// aaa

generate를 사용하여 생성된 스트림은 크기가 정해져 있지 않고 계속 생성하기 때문에 limit으로 개수를 제한해야 합니다.

Stream.iterate()

Stream.iterate(10, n -> n + 2).limit(3);
// 10 12 14

요소가 다음 요소의 입력으로 들어가 실행됩니다.

병렬 스트림

Stream<String> stream = str.parallelStream();

Arrays.stream(arr).parallel();

병렬 스트림은 내부적으로 Fork/Join Pool을 사용합니다.
병렬 스트림에서 순차 스트림으로 다시 되돌리려면 .sequential()을 사용하면 됩니다.

병렬 스트림이 프로세스로 나눠서 처리하기 때문에 언제나 빠를 것 같지만
항상 병렬 스트림이 순차 스트림보다 빠른 것은 아닙니다.

병렬 스트림은 스트림을 분할해 스레드에게 할당하고 처리 후 다시 합쳐야하는 과정이 추가되기 때문에 요소의 수가 적다면 오히려 속도가 느릴 수 있습니다.

이 외의 속도의 영향을 주는 요소

  • 스트림의 종류
  • 코어의 수
  • 박싱, 언박싱 유무
  • iterate와 같은 병렬로 처리하기 힘든 스트림

스트림 가공

전체 요소 중 원하는 요소만 뽑아내거나 요소를 변환해서 원하는 요소로 만드는 작업입니다. 리턴값으로 스트림을 반환하기 때문에 체이닝이 가능합니다.
특징은 지연 연산을 하기 때문에 결과 연산이 실행되기 전에는 아무 연산도 수행하지 않다가 결과 연산을 실행하게 되면 한 번에 처리합니다.

이렇게 스트림을 가공하는 중간 연산을 하나로 병합시켜 실행하는 방식을 루프 퓨전이라고 합니다.

List<String> names = Arrays.asList("hyunsu", "youngki", "jaehyun");

이 리스트를 사용해 실습하겠습니다.

Filtering

names.stream().filter(name -> name.contains("hyun"))
.forEach(System.out::println);
// hyunsu jaehyun

filter는 스트림 내의 요소들 중 해당하는 요소만 반환합니다.
이름에 hyun이 포함된 이름만 반환된 것을 확인할 수 있습니다.
filter함수의 인자로 받는 Predicate는 boolean을 리턴하는 평가식입니다.

Mapping

names.stream().map(name -> name+" hi").forEach(System.out::println);
// hyunsu hi youngki hi jaehyun hi

map은 스트림 내의 요소들을 변환해줍니다.
name에 hi를 추가해 반환하는 것을 확인할 수 있습니다.

Stream<Integer> stream = productList.stream().map(Product::getAmount);

이렇게 객체 안에 있는 함수를 사용할 수도 있습니다.

Sorting

IntStream.of(35, 20, 4, 99, 36)
	.sorted()
    .boxed()
    .collect(Collectors.toList());
    // 4, 20, 35, 36, 99

다른 정렬과 마찬가지로 Comparator를 사용합니다.
Comparator.reverseOrder()를 사용하면 내림차순으로 정렬합니다.

lang.stream()
  .sorted(Comparator.comparingInt(String::length))
  .collect(Collectors.toList());
  // [Go, Java, Scala, Swift, Groovy, Python]

lang.stream()
  .sorted((s1, s2) -> s2.length() - s1.length())
  .collect(Collectors.toList());
  // [Groovy, Python, Scala, Swift, Java, Go]

위와 같이 직접 Comparator를 구현해서 사용할 수 있습니다.

Peeking

peek 함수는 특정 결과를 반환하지 않고 작업을 수행하기만 합니다.
작업 중간에 결과를 확인해볼 때 사용할 수 있습니다.

스트림 결과

중간 연산을 종료하고 최종 작업을 하는 작업입니다.
맨 마지막에 한 번만 사용합니다.

Calculating

long count = IntStream.of(1, 3, 5, 7, 9).count();
long sum = LongStream.of(1, 3, 5, 7, 9).sum();
OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
OptionalInt max = IntStream.of(1, 3, 5, 7, 9).max();
OptionalDouble avarage = IntStream.of(1, 3, 5, 7, 9).average();

최소, 최대, 합, 평균으로 결과를 만들어낼 수 있습니다.
최소, 최대, 평균의 경우에는 스트림이 비어있다면 결과를 낼 수 없기 때문에 Optional로 반환합니다.

Collecting

collect 메소드는 Collector 타입에 인자를 받아서 처리를 하여 최종 작업을 합니다.

Collectors.toList()
스트림에서 작업한 결과를 리스트로 반환합니다.

List<String> collectorCollection =
  productList.stream()
    .map(Product::getName)
    .collect(Collectors.toList());
// [potatoes, orange, lemon, bread, sugar]

toArray(), toSet() 으로 배열이나 셋으로 변환할 수도 있습니다.

Collectors.joining()
스트림에서 작업한 결과를 하나의 문자열로 반환합니다.

String listToString = 
 productList.stream()
  .map(Product::getName)
  .collect(Collectors.joining(", ", "<", ">"));
// <potatoes, orange, lemon, bread, sugar>

첫 번째 파라미터는 요소를 구분해주는 구분자,
두 번째 파라미터는 접두사,
세 번째 파라미터는 접미사입니다.

Collectors.averagingInt()
Int 스트림의 평균을 구할 수 있습니다.

Double averageAmount = 
 productList.stream()
  .collect(Collectors.averagingInt(Product::getAmount));

Matching

매칭은 람다 Predicate를 받아서 조건에 만족하는 요소가 있는지 체크한 결과를 반환합니다.

  • anyMatch : 하나라도 조건에 만족하는지
  • allMatch : 모든 조건에 만족하는지
  • noneMatch : 모든 조건에 만족하지 않는지

boolean으로 결과가 반환됩니다.

List<String> names = Arrays.asList("Eric", "Elena", "Java");

boolean anyMatch = names.stream()
  .anyMatch(name -> name.contains("a"));
boolean allMatch = names.stream()
  .allMatch(name -> name.length() > 3);
boolean noneMatch = names.stream()
  .noneMatch(name -> name.endsWith("s"));

참고 문서

https://futurecreator.github.io/2018/08/26/java-8-streams/
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html

profile
잘하는 개발자(희망)

0개의 댓글