JAVA

JAVA8 | 스트림 API

TECH 톡마스터 2023. 6. 16.

JAVA8 | 스트림 API
JAVA8 스트림 API

 

 안녕하세요, 여러분! 오늘은 자바의 스트림 API에 관해 함께 알아보도록 하겠습니다. 스트림 API는 자바 8부터 추가된 강력한 기능으로, 데이터 처리를 간편하고 효율적으로 할 수 있게 도와줍니다. 많은 예제와 실용적인 활용 사례를 통해 스트림 API의 장점과 사용법을 자세히 알아보겠습니다. 함께 시작해 볼까요?

 

1. 스트림 API 소개

스트림 API는 자바 8에서 도입된 기능으로, 데이터 처리를 보다 효율적이고 간결하게 수행할 수 있도록 도와줍니다. 스트림은 데이터의 흐름을 나타내며, 데이터 소스를 추상화하여 다양한 연산을 수행할 수 있습니다. 이를 통해 코드의 가독성과 유지 보수성을 향상시킬 수 있습니다.

1.1 스트림 개요

스트림은 연속된 요소로 이루어진 데이터 처리 파이프라인입니다. 스트림은 데이터를 소스로부터 추출하여 중개 연산과 최종 연산을 적용하여 처리합니다. 스트림은 기본적으로 데이터를 변경하지 않고 원본 데이터를 유지하며, 지연 연산을 수행하여 최적의 성능을 제공합니다.

1.2 스트림의 특징

  • 스트림은 데이터 소스에 대한 추상화 계층입니다. 배열, 컬렉션, I/O 채널 등 다양한 데이터 소스를 스트림으로 다룰 수 있습니다.
  • 스트림은 중개 연산과 최종 연산으로 구성됩니다. 중개 연산은 스트림의 요소를 변환하거나 필터링하는 역할을 수행하며, 최종 연산은 스트림의 결과를 도출하거나 수집합니다.
  • 스트림은 내부 반복을 사용하여 데이터를 처리합니다. 내부 반복은 스트림 API가 요소의 처리를 알아서 처리하므로 개발자는 간결한 코드를 작성할 수 있습니다.
  • 스트림은 병렬 처리를 통해 성능을 향상시킬 수 있습니다. 데이터를 병렬로 처리하면 다중 스레드를 활용하여 작업을 분할하고 병렬로 처리함으로써 처리 시간을 단축할 수 있습니다.

1.3 스트림과 컬렉션의 비교

스트림과 컬렉션은 모두 데이터를 다루는데 사용되지만, 몇 가지 중요한 차이점이 있습니다.

  • 컬렉션은 데이터의 모음이지만, 스트림은 데이터의 흐름입니다.
  • 컬렉션은 데이터를 저장하고 접근하는 자료구조입니다. 반면에, 스트림은 데이터 소스로부터 추출하여 처리하는 개념입니다.
  • 컬렉션은 데이터를 변경하고 수정할 수 있지만, 스트림은 원본 데이터를 유지하면서 중개 연산과 최종 연산을 통해 데이터를 처리합니다.
  • 컬렉션은 외부 반복을 사용하여 데이터를 처리하는 반면, 스트림은 내부 반복을 사용하여 개발자는 반복 로직을 명시하지 않아도 됩니다.

이 를 통해 스트림은 보다 간결하고 선언적인 코드를 작성할 수 있으며, 병렬 처리에 대한 기능도 제공합니다.

2. 스트림 생성과 변환

2.1 컬렉션으로부터 스트림 생성

 컬렉션은 스트림을 생성하는 가장 일반적인 방법 중 하나입니다. 자바의 컬렉션 프레임워크는 stream() 메서드를 제공하여 컬렉션을 스트림으로 변환할 수 있습니다. 다음은 컬렉션으로부터 스트림을 생성하는 예제입니다.

List<String> names = Arrays.asList("John", "Sarah", "Michael");
Stream<String> stream = names.stream();

위 예제에서는 문자열로 이루어진 리스트 names를 생성하고, stream() 메서드를 호출하여 names 리스트를 스트림으로 변환합니다. 변환된 스트림은 Stream 타입의 객체인 stream 변수에 할당됩니다.

2.2 배열로부터 스트림 생성

배열은 또 다른 스트림 생성 방법입니다. 자바의 배열은 Arrays 유틸리티 클래스의 stream() 메서드를 사용하여 스트림으로 변환할 수 있습니다. 다음은 배열로부터 스트림을 생성하는 예제입니다.

int[] numbers = {1, 2, 3, 4, 5};
IntStream stream = Arrays.stream(numbers);

위 예제에서는 int 타입으로 이루어진 배열 numbers를 생성하고, Arrays.stream() 메서드를 호출하여 numbers 배열을 스트림으로 변환합니다. 변환된 스트림은 IntStream 타입의 객체인 stream 변수에 할당됩니다.

2.3 빈 스트림과 단일 요소 스트림 생성

빈 스트림(empty stream)은 요소가 없는 스트림을 말합니다. Stream 클래스의 정적 메서드인 empty()를 사용하여 빈 스트림을 생성할 수 있습니다. 다음은 빈 스트림을 생성하는 예제입니다.

Stream<String> emptyStream = Stream.empty();

 위 예제에서는 Stream.empty() 메서드를 호출하여 빈 스트림을 생성합니다. 이렇게 생성된 스트림은 Stream 타입의 객체 emptyStream 변수에 할당됩니다.

단일 요소 스트림(single element stream)은 하나의 요소를 포함하는 스트림을 말합니다. Stream 클래스의 정적 메서드인 of()를 사용하여 단일 요소 스트림을 생성할 수 있습니다. 다음은 단일 요소 스트림을 생성하는 예제입니다.

Stream<String> singleElementStream = Stream.of("Hello");

위 예제에서는 Stream.of() 메서드를 호출하고 문자열 "Hello"를 인자로 전달하여 단일 요소 스트림을 생성합니다. 이렇게 생성된 스트림은 Stream 타입의 객체인 singleElementStream 변수에 할당됩니다.

2.4 스트림 변환과 연결

 스트림은 중개 연산과 최종 연산을 통해 데이터를 처리합니다. 중개 연산은 스트림을 변환하고 필터링하는 역할을 수행하며, 최종 연산은 스트림의 결과를 도출하거나 수집합니다. 스트림은 연속된 연산을 수행할 수 있기 때문에, 다양한 중개 연산을 연결하여 스트림을 변환하거나 필터링할 수 있습니다. 최종 연산은 스트림을 소모하므로 중개 연산 이후에 위치해야 합니다. 예를 들어, 아래 코드는 리스트 names에서 길이가 5 이상인 문자열만 필터링하여 출력하는 예제입니다.

List<String> names = Arrays.asList("John", "Sarah", "Michael");
names.stream()
     .filter(name -> name.length() >= 5)
     .forEach(System.out::println);

위 코드에서는 names 리스트로부터 스트림을 생성하고, filter() 메서드를 호출하여 길이가 5 이상인 문자열만 필터링합니다. 그리고 forEach() 메서드를 호출하여 필터링된 결과를 출력합니다. 중개 연산인 filter() 메서드는 스트림을 변환하고 필터링하는 역할을 수행하며, 최종 연산인 forEach() 메서드는 스트림의 요소를 소모하여 출력합니다. 이처럼 중개 연산과 최종 연산을 연결하여 원하는 데이터 처리를 수행할 수 있습니다.

3. 중개 연산

3.1 필터링 (Filtering)

 필터링은 스트림에서 원하는 요소만 선택하여 추출하는 작업을 말합니다. 자바 8에서는 filter() 메서드를 사용하여 필터링을 수행할 수 있습니다. 필터링은 주어진 조건에 따라 요소를 검사하고, 조건을 만족하는 요소만을 유지합니다.

예를 들어, 다음 코드는 정수 리스트에서 짝수만을 필터링하여 새로운 리스트로 추출하는 예제입니다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
                                   .filter(number -> number % 2 == 0)
                                   .collect(Collectors.toList());

위 코드에서는 numbers 리스트로부터 스트림을 생성하고, filter() 메서드를 호출하여 짝수를 필터링합니다. 람다 표현식 number -> number % 2 == 0는 짝수 여부를 검사하는 조건입니다. 필터링된 결과는 evenNumbers 리스트에 수집되어 새로운 리스트를 생성합니다.

3.2 매핑 (Mapping)

매핑은 스트림의 각 요소를 다른 요소로 변환하는 작업을 말합니다. 매핑은 주어진 함수를 사용하여 요소를 변환하고, 변환된 요소를 유지합니다. 자바 8에서는 map() 메서드를 사용하여 매핑을 수행할 수 있습니다. 예를

들어, 다음 코드는 문자열 리스트의 각 요소를 대문자로 변환하여 새로운 리스트로 매핑하는 예제입니다.

List<String> names = Arrays.asList("John", "Sarah", "Michael");
List<String> upperCaseNames = names.stream()
                                   .map(String::toUpperCase)
                                   .collect(Collectors.toList());

위 코드에서는 names 리스트로부터 스트림을 생성하고, map() 메서드를 호출하여 각 문자열을 대문자로 매핑합니다. String::toUpperCase는 문자열을 대문자로 변환하는 메서드 참조입니다. 매핑된 결과는 upperCaseNames 리스트에 수집되어 새로운 리스트를 생성합니다.

3.3 정렬 (Sorting)

정렬은 스트림의 요소를 특정 기준에 따라 정렬하는 작업을 말합니다. 자바 8에서는 sorted() 메서드를 사용하여 정렬을 수행할 수 있습니다.

예를 들어, 다음 코드는 문자열 리스트를 알파벳 순서로 정렬하는 예제입니다.

List<String> names = Arrays.asList("John", "Sarah", "Michael");
List<String> sortedNames = names.stream()
                                .sorted()
                                .collect(Collectors.toList());

위 코드에서는 names 리스트로부터 스트림을 생성하고, sorted() 메서드를 호출하여 알파벳 순서로 정렬합니다. 정렬된 결과는 sortedNames 리스트에 수집되어 새로운 리스트를 생성합니다.

3.4 제한과 스킵 (Limit and Skip)

제한과 스킵은 스트림의 요소를 제한하거나 일부 요소를 건너뛰는 작업을 말합니다. 자바 8에서는 limit() 메서드와 skip() 메서드를 사용하여 제한과 스킵을 수행할 수 있습니다.

예를 들어, 다음 코드는 정수 리스트에서 처음 3개의 요소만 추출하는 예제입니다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> limitedNumbers = numbers.stream()
                                      .limit(3)
                                      .collect(Collectors.toList());

위 코드에서는 numbers 리스트로부터 스트림을 생성하고, limit(3) 메서드를 호출하여 처음 3개의 요소만 제한합니다. 제한된 결과는 limitedNumbers 리스트에 수집되어 새로운 리스트를 생성합니다.

또한, skip() 메서드를 사용하여 일부 요소를 건너뛸 수도 있습니다. 예를 들어, 다음 코드는 정수 리스트에서 처음 3개의 요소를 건너뛰고 나머지 요소들을 추출하는 예제입니다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> skippedNumbers = numbers.stream()
                                      .skip(3)
                                      .collect(Collectors.toList());

위 코드에서는 numbers 리스트로부터 스트림을 생성하고, skip(3) 메서드를 호출하여 처음 3개의 요소를 건너뜁니다. 건너뛴 결과는 skippedNumbers 리스트에 수집되어 새로운 리스트를 생성합니다.

3.5 중복 제거 (Distinct)

중복 제거는 스트림에서 중복된 요소를 제거하는 작업을 말합니다. 자바 8에서는 distinct() 메서드를 사용하여 중복 제거를 수행할 수 있습니다.예를 들어, 다음 코드는 정수 리스트에서 중복된 요소를 제거하는 예제입니다.

List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 4, 5);
List<Integer> distinctNumbers = numbers.stream()
                                       .distinct()
                                       .collect(Collectors.toList());

위 코드에서는 numbers 리스트로부터 스트림을 생성하고, distinct() 메서드를 호출하여 중복된 요소를 제거합니다. 중복 제거된 결과는 distinctNumbers 리스트에 수집되어 새로운 리스트를 생성합니다.

중개 연산은 스트림의 요소를 변환하고 필터링하는 다양한 작업을 수 행할 수 있습니다. 이를 통해 데이터를 가공하고 원하는 결과를 도출할 수 있습니다. 각 중개 연산에 대한 예제 코드와 설명을 통해 자세히 알아보았습니다. 이를 참고하여 스트림을 효과적으로 활용해 보세요.

4. 최종 연산 (Terminal Operations)

4.1 요소 검색과 매칭 (Element Searching and Matching)

스트림의 최종 연산 중 일부는 요소 검색과 매칭에 사용됩니다. 이러한 연산들은 스트림의 요소들을 조사하고, 조건에 맞는 요소를 찾거나 특정 조건을 만족하는지 확인하는 작업을 수행합니다.

  • anyMatch(): 스트림의 요소 중 최소한 하나의 요소가 주어진 조건을 만족하는지 확인합니다.
  • allMatch(): 스트림의 모든 요소가 주어진 조건을 만족하는지 확인합니다.
  • noneMatch(): 스트림의 모든 요소가 주어진 조건을 만족하지 않는지 확인합니다.
  • findFirst(): 스트림에서 첫 번째 요소를 반환합니다.
  • findAny(): 스트림에서 임의의 요소를 반환합니다.

다음은 이러한 요소 검색과 매칭 연산의 예제 코드와 설명입니다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

boolean anyMatch = numbers.stream().anyMatch(n -> n > 3);
// numbers 스트림의 요소 중에서 3보다 큰 값이 최소한 하나 존재하는지 확인

boolean allMatch = numbers.stream().allMatch(n -> n > 0);
// numbers 스트림의 모든 요소가 0보다 큰지 확인

boolean noneMatch = numbers.stream().noneMatch(n -> n < 0);
// numbers 스트림의 모든 요소가 0보다 작지 않은지 확인

Optional<Integer> first = numbers.stream().findFirst();
// numbers 스트림의 첫 번째 요소를 반환

Optional<Integer> any = numbers.stream().findAny();
// numbers 스트림에서 임의의 요소를 반환

4.2 요소 수집 (Element Collecting)

요소 수집은 스트림의 요소들을 수집하여 다른 형태로 변환하거나 그룹화하는 작업을 의미합니다. 자바 8에서는 collect() 메서드를 사용하여 요소 수집을 수행할 수 있습니다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

List<Integer> evenNumbers = numbers.stream()
                                   .filter(n -> n % 2 == 0)
                                   .collect(Collectors.toList());
// numbers 스트림에서 짝수인 요소들을 수집하여 리스트로 반환

Set<Integer> uniqueNumbers = numbers.stream()
                                    .collect(Collectors.toSet());
// numbers 스트림의 중복된 요소를 제거하고 유일한 값들로 이루어진 Set으로 반환

Map<Integer, List<Integer>> groupedNumbers = numbers.stream()
                                                    .collect(Collectors.groupingBy(n -> n % 2));
// numbers 스트림의 요소들을 홀수와 짝수로 그룹화하여 Map으로 반환

4.3 리덕션과 요약 (Reduction and Summarization)

리덕션과 요약 연산은 스트림의 요소들을 하나의 값으로 축소하는 작업을 의미합니다. 스트림의 모든 요소를 대상으로 연산을 수행하고 최종 결과를 반환합니다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

int sum = numbers.stream().reduce(0, (a, b) -> a + b);
// numbers 스트림의 모든 요소를 더하여 합계를 계산

Optional<Integer> max = numbers.stream().reduce(Integer::max);
// numbers 스트림의 최댓값을 반환

long count = numbers.stream().count();
// numbers 스트림의 요소 개수를 반환

IntSummaryStatistics statistics = numbers.stream().mapToInt(Integer::intValue).summaryStatistics();
// numbers 스트림의 요소들에 대한 통계 정보를 제공 (최솟값, 최댓값, 합계, 평균, 개수)

4.4 그룹화와 분할 (Grouping and Partitioning)

스트림 API는 요소들을 그룹화하거나 분할하는 기능도 제공합니다. groupingBy()와 partitioningBy() 메서드를 사용하여 요소들을 특정 기준에 따라 그룹화하거나 분할할 수 있습니다.

List<String> names = Arrays.asList("John", "Sarah", "Tom", "Mary");

Map<Character, List<String>> namesByFirstLetter = names.stream()
                                                       .collect(Collectors.groupingBy(name -> name.charAt(0)));
// names 스트림의 요소들을 첫 번째 글자로 그룹화하여 Map으로 반환

Map<Boolean, List<String>> namesByLength = names.stream()
                                                .collect(Collectors.partitioningBy(name -> name.length() > 3));
// names 스트림의 요소들을 이름 길이가 3보다 큰 것과 작은 것으로 분할하여 Map으로 반환

4.5 병렬 스트림 (Parallel Streams)

스트림 API는 병렬 처리를 지원하기 위해 병렬 스트림(Parallel Streams)을 제공합니다. 병렬 스트림을 사용하면 요소들을 병렬적으로 처리하여 성능을 향상시킬 수 있습니다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

int sum = numbers.parallelStream().reduce(0, Integer::sum);
// numbers 병렬 스트림의 모든 요소를 더하여 합계를 계산

Optional<Integer> max = numbers.parallelStream().max(Integer::compare);
// numbers 병렬 스트림의 최댓값을 반환

이렇게 다양한 최종 연산을 통해 스트림의 요소들을 검색하고 수집하며, 리덕션하고 그룹화하며, 병렬 처리할 수 있습니다. 이를 활용하여 스트림 API의 다양한 기능을 유연하게 활용할 수 있습니다.

5. 스트림 API 활용

5.1 컬렉션 처리 (Collection Processing)

스트림 API를 사용하면 컬렉션의 요소들을 효율적으로 처리할 수 있습니다. 다양한 연산을 조합하여 원하는 결과를 얻을 수 있습니다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// 요소 필터링과 매핑을 조합하여 짝수의 제곱 값 리스트를 생성
List<Integer> evenSquared = numbers.stream()
                                   .filter(n -> n % 2 == 0)
                                   .map(n -> n * n)
                                   .collect(Collectors.toList());

위의 예제 코드에서는 numbers라는 정수형 리스트에서 스트림을 생성하고, filter 메서드를 사용하여 짝수만을 필터링합니다. 그 후, map 메서드를 사용하여 각 요소의 제곱 값을 계산합니다. 마지막으로 collect 메서드를 사용하여 결과를 리스트로 수집합니다. 이렇게 함께 사용되는 연산들을 체인으로 연결하여 원하는 결과를 간단하게 얻을 수 있습니다.

5.2 데이터 필터링 (Data Filtering)

스트림 API를 사용하면 데이터를 효과적으로 필터링할 수 있습니다. 원하는 조건에 맞는 데이터만을 추출하거나 걸러낼 수 있습니다.

List<String> names = Arrays.asList("John", "Sarah", "Tom", "Mary");

// 이름 길이가 4 이상인 데이터 필터링
List<String> filteredNames = names.stream()
                                  .filter(name -> name.length() >= 4)
                                  .collect(Collectors.toList());

위의 예제 코드에서는 names라는 문자열 리스트에서 스트림을 생성하고, filter 메서드를 사용하여 이름의 길이가 4 이상인 데이터만을 필터링합니다. 그 후, collect 메서드를 사용하여 결과를 리스트로 수집합니다.

5.3 매핑과 변환 (Mapping and Transformation)

스트림 API를 사용하면 요소들을 다른 형태로 매핑하거나 변환할 수 있습니다. 요소들을 다른 객체로 변환하거나 특정 속성을 추출할 수 있습니다.

List<Person> people = Arrays.asList(
    new Person("John", 25),
    new Person("Sarah", 30),
    new Person("Tom", 20)
);

// 이름만을 추출하여 리스트 생성
List<String> names = people.stream()
                           .map(Person::getName)
                           .collect(Collectors.toList());

위의 예제 코드에서는 people이라는 Person 객체 리스트에서 스트림을 생성하고, map 메서드를 사용하여 각 요소의 이름만을 추출합니다. 그 후, collect 메서드를 사용하여 결과를 리스트로 수집합니다.

5.4 요약과 통계 (Summarization and Statistics)

스트림 API를 사용하면 요소들에 대한 요약 정보와 통계 정보를 손쉽게 계산할 수 있습니다. 합계, 평균, 최댓값, 최솟값 등을 쉽게 구할 수 있습니다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// 합계 계산
int sum = numbers.stream()
                 .mapToInt(Integer::intValue)
                 .sum();

// 평균 계산
OptionalDouble average = numbers.stream()
                                .mapToDouble(Integer::doubleValue)
                                .average();

위의 예제 코드에서는 numbers라는 정수형 리스트에서 스트림을 생성하고, mapToInt 메서드를 사용하여 정수로 매핑합니다. 그 후, sum 메서드를 사용하여 합계를 계산합니다. 평균은 mapToDouble 메서드를 사용하여 실수형으로 매핑한 후 average 메서드를 사용하여 계산합니다. 계산된 결과는 OptionalDouble 타입으로 반환됩니다.

5.5 병렬 처리 (Parallel Processing)

스트림 API는 병렬 처리를 지원하여 작업을 분산하여 빠른 처리를 할 수 있습니다. 병렬 스트림을 사용하여 요소들을 동시에 처리할 수 있습니다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// 요소 필터링과 매핑을 병렬로 처리하여 결과 리스트 생성
List<Integer> result = numbers.parallelStream()
                              .filter(n -> n % 2 == 0)
                              .map(n -> n * n)
                              .collect(Collectors.toList());

위의 예제 코드에서는 numbers라는 정수형 리스트에서 병렬 스트림을 생성하고, filter 메서드를 사용하여 짝수만을 필터링합니다. 그 후, map 메서드를 사용하여 각 요소의 제곱 값을 계산합니다. 마지막으로 collect 메서드를 사용하여 결과를 리스트로 수집합니다. 병렬 스트림을 사용하면 작업들이 동시에 처리되어 처리 속도를 향상시킬 수 있습니다.

 

 이제 우리는 스트림 API에 대해 풍부한 지식을 얻었습니다. 스트림을 생성하고 변환하며, 중개 연산과 최종 연산을 활용하여 데이터를 처리하는 방법을 배웠습니다. 스트림 API를 통해 컬렉션 처리, 데이터 필터링, 매핑, 요약, 그룹화, 병렬 처리 등 다양한 작업을 쉽고 간결하게 수행할 수 있습니다.

이제 여러분은 스트림 API의 강력함과 유연성을 활용하여 자바 프로그래밍을 더욱 효율적으로 개발할 수 있습니다. 자바 개발자로서 스트림 API에 익숙해지면 코드의 가독성과 유지 보수성을 향상시키는 데 큰 도움이 될 것입니다.

댓글

💲 추천 글