[자바] 스트림(Stream)에 대해서 알아보자

tech_bae·2025년 3월 18일

Java

목록 보기
8/10
post-thumbnail

데브코스 수업에선 구현도 하고 그랬지만, 스트림이 뭔지도 몰랐던 나에겐 무리였다. 고로 이 글에서 스트림이 뭐고 어떻게 그리고 왜 쓰는지 정리해보겠다.

스트림(Stream)이란

  • 자료구조(컬렉션, 배열 등)을 쉽게 처리할 수 있도록 도와주는 기능
  • 데이터를 반복문 없이 연속적으로 처리할 수 있게 함

기존의 for문 이나 iterator를 사용하면 코드가 길어짐

가독성 재사용성 떡락 또한 데이터 타입마다 다르게 다루어야한다 귀찮다.

⇒ 스트림은 데이터 소스에 상관없이(데이터 소스를 추상화) 모두 같은 방식으로 다룰 수 있다.

스트림의 특징

  • 데이터를 한 줄씩 처리 : 자료구조 전체를 메모리에 올리지 않고, 하나씩 순차적으로 처리
  • 불변성 : 기존의 데이터 변경 ❌, 새로운 데이터 반환
  • 람다식 사용()
  • 중간 연산과 최종 연산 : 데이터를 가공 → 중간 연산, 결과 출력 → 최종연산
    • 지연 연산 사용 ⇒ 최종 연산이 호출되어야 중간 연산이 수행된다.

스트림 사용하기

스트림 만들기

스트림을 사용하려면 스트림을 만들어야겠죠.

어떤 스트림을 만드느냐에 따라서 방법이 조금씩 다릅니다.

  • 배열 스트림 : Arrays.stream(배열명)으로 만들 수 있습니다.
String[] strArr = {"a", "b", "c", "d", "e", "f", "g", "h"};
Stream<String> arrStream = Arrays.stream(strArr);
  • 컬렉션 스트림: .stream()으로 생성합니다.
LinkedList<String> linkedList = new LinkedList<>();
Stream<String> linkedListStream = linkedList.stream();

ArrayList<Integer> arrayList = new ArrayList<>();
Stream<Integer> arrayListStream = arrayList.stream();
  • 직접 스트림 생성 : 배열이나 컬렉션 없이 직접 스트림 생성(builder(), Stream.of())
//builder 사용
Stream<String> builderStream = Stream.<String>builder()
      .add("a").add("b")
      .build();
      
//Stream.of()사용
Stream<String> streamOf = Stream.of("Hello", "World");
  • 람다사용 : 람다를 사용하여 스트림을 생성 할 수도 있다. (Stream.generate(), Stream.iterate())
  1. Stream.generate()

    Stream<Integer> randomStream = Stream.generate(() -> (int) (Math.random() * 100)).limit(3);
    

    Stream.generate()Supplier<T>람다식을 이용하므로 값을 공급하는(단지 생성하는) 스트림을 만들때 사용한다.

    limit()를 사용하지 않으면 무한으로 값이 생성된다 → 무한스트림!

  1. Stream.iterate()

    Stream<Integer> iterateStream = Stream.iterate(0, n -> n + 2).limit(3

    Stream.iterate()는 매개변수가 1개 있는 UnaryOperator람다식을 사용해서 초기값으로 연산식을 통해 연산되는 값이 연속적으로 생성된다.

    또한, generate()와 동일하게 limit()를 사용하여 개수를 제한할 수 있다.

  • 기본 타입형 스트림 : 기본형 스트림도 만들 수 있다!! (IntStream, LongStream, DoubleStream)
IntStream intStream = IntStream.range(1,5);
DoubleStream doubleStream = DoubleStream.of(1.2,15.8,2154.8487);

중간연산

데이터를 가공하는 과정(변형, 필터링… 등)

중간 연산은 메서드체이닝이 된다. (연속적으로 수행)

  • filter() : 조건에 맞는 데이터 선택(필터링) , if문과 비슷한 역할
    List<Integer> list = Arrays.asList(1,2,3,4,5);
    Stream<Integer> result = list.stream().filter(n -> n > 3);
    result.forEach(System.out::println);
    
    /*출력
    4
    5
    */
  • map(): 데이터를 변환
    List<String> strList = Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h");
    Stream<String> strStream = strList.stream().map(l -> l.toUpperCase());
    
    strStream.forEach(l -> System.out.print(l+" "));
    
    /*출력
    A B C D E F G H 
  • sorted() : 데이터 정렬 (Comparator를 사용한다.)
    List<String> words = List.of("apple", "banana", "blueberry", "oranges");
    Stream<String> wordsStream = words.stream()
            .sorted((s1, s2) ->  Integer.compare(s1.length(), s2.length()));
    wordsStream.forEach(System.out::println);
    
    //Comparator.comparing()을 사용하면 더 간결
    Stream<String> sortedWords = words.stream()
            .sorted(Comparator.comparing(String::length));  // 문자열 길이 기준 정렬
    sortedWords.forEach(System.out::println);
    
    /*출력
    apple
    banana
    oranges
    blueberry
    */
  • distinct() : 중복제거
  • skip(n) : 앞의 n개 건너뛰기
  • peek() : 중간 상태 확인용(디버깅)
    List<String> listForRests = Arrays.asList("a", "a", "c", "d", "e", "f", "f", "h");
    Stream<String> wordsStreamForRests = listForRests.stream().distinct()
            .limit(4)
            .peek(s -> System.out.println("중간확인 : " + s))
            .skip(2);
    wordsStreamForRests.forEach(System.out::println);

최종 연산

스트림을 처리하여 최종 결과 반환

최종 연산 이 후엔 스트림이 소모(사용)되므로, 한번만 실행 가능

  • 통계내기(기본형 타입만 가능)
    //count()로 개수 반환 long타입반환
    long count = IntStream.of(grades).count();
          
    //sum()으로 합계 구하기
    int summed = IntStream.of((grades)).sum();
    
    //min() 최솟값 : OptionalInt반환 (null값 대비)
    OptionalInt minimum = IntStream.of(grades).min();
    
    //max() 최댓값 : OptionalInt반환 (null값 대비)
    OptionalInt maximum = IntStream.of(grades).max();
    
    //average() 최솟값 : OptionalDouble반환 (null값 대비)
    OptionalDouble avg = IntStream.of(grades).average();
        
  • collect(): 스트림 요소 원하는 자료형으로 변환
    List<String> list = List.of("apple", "banana", "cherry", "apple");
    
    // List -> Set 변환 (중복 제거)
    Set<String> set = list.stream()
                    .collect(Collectors.toSet());
  • reduce() : 스트림의 요소를 누적하여 하나로 합친다.
    • 초기값이 있으면 T 반환, 없으면 Optional<T>를 반환한다. (null값 대비)

      List<Integer> numbers = List.of(1, 2, 3, 4, 5);
      //초기값이 없으므로 Optional<T>를 반환한다.
      Optional<Integer> sum = numbers.stream()
                       .reduce((a, b) -> a + b); //15
                       
      //초기값 있으므로 Optional불필요(null일 수가 없음)                 
      int sumWithInitial = numbers.stream()
                      .reduce(5,(a, b) -> a + b); //20                       
  • 매칭(anyMatch(), allMatch(), noneMatch())
    • 특정 조건을 만족하는 요소가 있는치 체크

    • boolean반환

      //길이가 4이상인 요소가 하나라도 있는지
      boolean any = members.stream().anyMatch(name -> name.length() >= 4);
      System.out.println(any); // true
      
      //모든 요소가 "e"를 포함하는지
      boolean all = members.stream().allMatch(name -> name.contains("e"));
      System.out.println(all); // false
      
      //q로 끝나는 요소가 하나도 없는지
      boolean noneMatched = members.stream().noneMatch(name -> name.endsWith("q"));
      System.out.println(noneMatched); // true

병렬 스트림(Parallel Stream)

여러 개의 스레드를 사용하여 데이터를 병렬로 처리가 가능!

parallelStream() 이나 parallel()을 이용해 병렬 스트림 사용

순서가 보장되지 않는다. (순서가 보장되어야 한다면 주의해야 한다.)

⇒ 즉, 기본 스트림은 순차적으로 하나씩 데이터를 처리하지만 병렬스트림은 여러 스레드에서 동시에 데이터를 처리한다!

List<String> list = List.of("A", "B", "C", "D", "E");

list.parallelStream()
        .forEach(s -> System.out.println(Thread.currentThread().getName() + " - " + s));

/*
출력
ForkJoinPool.commonPool-worker-3 - A
ForkJoinPool.commonPool-worker-1 - B
main - C
ForkJoinPool.commonPool-worker-2 - E
ForkJoinPool.commonPool-worker-5 - D
*/

parallel()은 기존 스트림을 병렬스트림으로 변환해준다.

list.stream()  // 순차 스트림
    .parallel()  // 병렬 처리로 변환
    .forEach(s -> System.out.println(Thread.currentThread().getName() + " - " + s));

이번 정리를 통해서 대충 스트림이 어떤 역할을 하는지는 알 것 같다.

하지만 아직 코드로 직접 사용하는데는 서툰거 같아서 틈틈히 일부로 스트림을 사용해봐야겠다.

또한 스트림을 알아보면서(사실 람다 공부했을 때) 메서드 참조(::) 가 근근히 보이는데 이것도 나중에 정리를 해봐야겠다. (멋져보이지 않는가!)

profile
전 아무고토 몰루고 아무고토 못해여

0개의 댓글