Home > Backend > Java > ☕️[Java] 스트림

☕️[Java] 스트림
Java Programming Language Backend

1️⃣ 스트림.

1. 스트림(Stream)

자바에서 스트림(Stream) API는 자바 8에서 도입되어 컬렉션의 요소를 선언적으로 처리할 수 있는 방법을 제공합니다.
스트림 API를 이용하면 데이터 요소의 시퀀스를 효율적으로 처리할 수 있으며, 데이터를 병렬로 처리하는 것도 간단할게 할 수 있습니다.
스트림을 이용하면 복잡한 데이터 처리 작업을 간결하고 명확한 코드로 작성할 수 있습니다.

1.2 스트림의 주요 특정.

  • 1. 선언적 처리 : 스트림을 사용하면 무엇을 할 것인지(what)에 집중하여 작업을 설명할 수 있고, 어떻게 처리할 것인지(how)는 스트림 API가 알아서 최적화하여 처리합니다.

  • 2. 파이프라이닝 : 스트림 연산은 파이프라인을 형성할 수 있으며, 여러 단계의 처리 과정을 연결하여 복잡한 데이터 처리를 효과적으로 할 수 있습니다.

  • 3. 내부 반복 : 스트림은 “내부 반복”을 사용합니다. 즉, 데이터를 어떻게 반복할지 스트림이 처리하므로, 개발자는 각 요소에 어떤 처리를 할지만 정의하면 됩니다.

  • 4. 불변성 : 스트림은 데이터를 수정하지 않습니다. 대신, 각 단계에서 결과를 내는 새로운 스트림을 생성합니다. 이는 함수형 프로그래밍의 특성을 반영합니다.

1.3 스트림의 작업 흐름.

스트림 API의 작업 흐름은 크게 세 부분으로 나눌 수 있습니다.

  • 1. 스트림 생성 : 컬렉션, 배열, I/O 자원 등의 데이터 소스로부터 스트림을 생성합니다.
    List<String> myList = Arrays.asList("a1", "a2", "b1", "b2", "c2", "c1");
    Stream<String> myStrean = myList.stream();
    
  • 2. 중간 연산(Intermediate operations) : 스트림을 변환하는 연산으로, 필터링, 매핑, 정렬 등이 있으며, 이 연산들은 연결 가능하고, 또한 게으르게(lazily) 실행됩니다.
    myStream.filter(s -> s.startsWith("c"))
          .map(String::toUpperCase)
          .sorted();
    
  • 3. 종단 연산(Terminal operations) : 스트림의 요소들을 소모하여 결과를 생성하는 연산입니다. 예를 들어, forEach, reduce, collect 등이 있으며, 이 연산을 수행한 후 스트림은 더 이상 사용할 수 없습니다.
    myStream.forEach(System.out::println);
    

1.4 스트림과 병렬 처리.

스트림 API는 병렬 처리를 간단하게 지원합니다.
‘paralleStream()’ 을 호출하면 자동으로 여러 쓰레드에서 스트림 연산이 병렬로 수행됩니다.
이는 데이터가 큰 경우에 유용하며, 멀티코어 프로세서의 이점을 쉽게 활용할 수 있게 해줍니다.

1.5 📝 정리.

스트림은 자바에서 데이터 컬렉션을 함수형 스타일로 쉽게 처리할 수 있게 하는 강력한 도구입니다.
이는 코드의 간결성과 가독성을 높이는 데 큰 도움을 줍니다.


2. 중개 연산(Intermediate operations)

자바 스트림 API에서 중개 연산(Intermediate operations)은 스트림의 요소들을 처리하고 변형하는 연산들로서, 다른 스트림을 반환합니다.

중개 연산은 게으른(lazy) 특성을 가지며, 종단 연산(Terminal operation)이 호출되기 전까지는 실제로 실행되지 않습니다.

이런 특성은 연산의 체인을 구성할 때 성능 최적화에 도움을 줍니다.

2.1 중개 연산의 주요 특성.

  • 게으른 실행(Lazy Execution) : 중개 연산은 호출되었을 때 즉시 실행되지 않습니다. 대신, 종단 연산이 호출될 때 까지 실행이 지연됩니다.

  • 스트림 변환 : 각 중개 연산은 변형된 형태의 새로운 스트림을 변환합니다. 이는 연산을 연쇄적으로 연결할 수 있도록 합니다.

2.2 주요 중개 연산의 종류.

  • 1. 필터링(Filtering)
    • ‘filter(Predicate<T> predicate)’ : 주어진 조건(프리디케이트)에 맞는 요소만을 포함하는 스트림을 반환합니다.
      List<String> names = Arrays.asList("Jo", "Lee", "Park", "Kang");
        names.stream()
         .filter(name -> name.startsWith("K"))
         .forEach(System.out::println); // 출력: "Kang"
      
  • 2. 매핑(Mapping)
    • ‘map(Function<T, R> mapper)’ : 스트림의 각 요소에 주어진 함수를 적용하고, 함수 결과로 주성된 새 스트림을 반환합니다.
    • ‘flatMap(Function<T, Stream<R>> mapper)’ : 각 요소에 함수를 적용한 결과로 생성된 여러 스트림을 하나의 스트림으로 평탄화합니다.
      List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
      numbers.stream()
       .map(number -> number * number)
       .forEach(System.out::println); //출력: 1, 4, 9, 16
      
  • 3. 정렬(Sorting)
    • ‘sorted() :’ 자연 순서대로 스트림을 정렬합니다.
    • ‘sorted(Comparator<T> comparator) :’ 주어진 비교자를 사용하여 스트림을 정렬합니다.
      List<String> fruits = Arrays.asList("banana", "apple", "orange", "kiwi");
      fruits.stream()
      .sorted()
      .forEach(System.out::println); // 출력: apple, banana, kiwi, orange
      
  • 4. 제한(Limiting) 및 건너뛰기(Skipping)
    • ‘limit(long maxSize)’ : 스트림의 요소를 주어진 크기로 제한합니다.
    • ‘skip(long n)’ : 스트림의 처음 n개 요소를 건너뜁니다.
      List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
      numbers.stream()
       .skip(2)
       .limit(3)
       .forEach(System.out::println); // 출력 3, 4, 5
      
  • 5. 중복 제거(Distinct)
    • ‘distinct()’ : 스트림에서 중복된 요소를 제거합니다.
      List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5);
      numbers.stream()
       .distinct()
       .forEach(System.out::println); // 출력: 1, 2, 3, 4, 5
      

2.3 📝 정리.

중개 연산을 통해 데이터 스트림을 세밀하게 제어하고 원하는 형태로 데이터를 변형 할 수 있습니다.
이러한 연산들은 다양한 데이터 처리 작업에서 매우 유용하게 사용됩니다.


3. 최종 연산(Terminal operations)

자바 스트림 API에서 최종 연산(Terminal operations)은 스트림 파이프라인의 실행을 트리거하고 스트림의 요소를 소비하여 결과를 생성하거나 부작용(side effect)을 일으키는 연산입니다.

최종 연산이 호출되기 전까지 중간 연산들은 게으른(lazy) 방식으로 처리되며 실행되지 않습니다.
최종 연산 후에는 스트림이 소비되어 더 이상 사용할 수 없게 됩니다.

3.1 최종 연산의 주요 유형.

  • 1. 수집(Collection)
    • ‘collect(Collector<T, A, R> collector)’ : 스트림의 요소를 변환, 결합하고 컬렉션으로 또는 다른 형태로 결과를 수집합니다.
      • 예를 들어, ‘toList()’, ‘toSet()’, ‘toMap()’ 등이 있습니다.
        List<String> names = Array.asList("Alice", "Bob", "Charlie", "David");
        List<String> list = names.stream()
                       .filter(name -> name.startsWith("A"))
                       .collect(Collectors.toList());
        System.out.println(list); // 출력 ["Alice"]
        
  • 2. 집계(Aggregation)
    • ‘count()’ : 스트림의 요소 개수를 반환합니다.
    • ‘max(Comparator<T> comparator)’ : 스트림에서 최대값을 찾습니다.
    • ‘min(Comparator<T> comparator)’ : 스트림에서 최소값을 찾습니다.
    • ‘reduce(BinaryOperator<T> accumulator)’ : 스트림의 요소를 결합하여 하나의 결과를 생성합니다.
      List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
      int sum = numbers.stream()
                 .reduce(0, (a, b) -> a + b);
      System.out.println(sum); // 출력: 10
      
  • 3. 반복(Iteration)
    • ‘forEach(Consumer<T> action)’ : 각 요소에 대해 주어진 작업을 수행합니다. 스트림의 순서대로 실행됩니다.
      List<String> names = Arrays.asList("Alist", "Bob", "Charlie", "David");
      names.stream()
         .forEach(System.out::println); // Alice, Bob, Charlie, David
      
  • 4. 조건 검사(Checking)
    • ‘allMatch(Predicate<T> predicate) :’ 모든 요소가 주어진 조건을 만족하는지 검사합니다.
    • ‘anyMatch(Predicate<T>predicate) :’ 어떤 요소라도 주어진 조건을 만족하는지 검사합니다.
    • ‘noneMatch(Predicate<T>predicate) :’ 모든 요소가 주어진 조건을 만족하지 않는지 검사합니다.
      boolean allEven = numbers.stream()
                         .allMatch(n -> n % 2 == 0);
      System.out.println(allEven); // 출력: false
      
  • 5. 요소 검색(Finding)
    • ‘findFirst()’ : 스트림의 첫 번째 요소를 Optional로 반환합니다.
    • ‘findAny()’ : 스트림에서 임의의 요소를 Optional로 반환합니다. 병렬 스트림에서 유용합니다.
      Optional<String> first = names.stream()
                              .findFirst();
      first.ifPresent(System.out::println); // 출력 Alice
      

3.2 📝 정리.

이러한 최종 연산들은 스트림 처리를 완료하고 필요한 결과를 도출하기 위해 사용됩니다.
스트림 API를 통해 데이터 처리를 선언적이고 간결하게 할 수 있으며, 복잡한 로직을 효과적으로 관리할 수 있습니다.