Computer >> 컴퓨터 >  >> 프로그래밍 >> Java

Java 8 스트림(Stream) 완벽 가이드: 실전 코드 예제로 배우기

이 글에서는 Java 8의 스트림(Streams) 기능을 자세히 살펴보고, 다양한 실전 코드 예제를 통해 개념을 쉽게 이해할 수 있도록 안내합니다.

Java 스트림은 함수형 프로그래밍을 Java에 도입한 기능으로, Java 8부터 지원됩니다. 따라서 그 이전 버전을 사용 중이라면 Java 스트림을 활용하기 위해 반드시 Java 8 이상으로 업그레이드해야 합니다.

Java 스트림, 왜 사용해야 할까요?

스트림을 사용하면 얻을 수 있는 주요 장점은 다음과 같습니다.

  • 적은 코드로 많은 작업 수행: 몇 줄의 코드만으로도 상당히 복잡한 데이터 처리 로직을 간결하게 구현할 수 있어 더 효율적인 Java 프로그래머가 될 수 있습니다.
  • 람다 표현식(Lambda Expression) 활용: 스트림은 일회성 함수라고 할 수 있는 람다 표현식을 폭넓게 활용합니다.
  • 병렬 처리 지원: ParallelStream을 사용하면 대량의 데이터셋에 대한 멀티스레드 연산을 매우 손쉽게 구현할 수 있습니다.

스트림 파이프라인(Streams Pipeline)

대부분의 경우 스트림 파이프라인은 다음 세 가지 요소로 구성됩니다.

  • 소스(Source): 데이터가 흘러나오는 원천
  • 중간 연산(Intermediate Operations): 0개 이상 연결 가능
  • 최종 연산(Terminal Operation): 단 하나만 허용

소스는 요소(element)들의 스트림을 생성합니다. 이렇게 만들어진 스트림은 필터링(filtering), 정렬(sorting), 매핑(mapping) 등 다양한 중간 연산을 거치며 각 요소를 가공할 수 있습니다.

마지막에는 수집(collect), 축소(reduce) 등의 최종 연산을 수행하며, 최종 연산은 단 하나만 실행된다는 점을 기억하세요.

스트림 소스(Stream Source)

스트림 소스는 컬렉션(Collection), 리스트(List), 셋(Set), int/long/double 배열, 문자열 등 다양한 형태로부터 생성할 수 있습니다.

스트림 연산(Stream Operations)

스트림 연산은 크게 중간 연산과 최종 연산으로 나뉩니다.

  • 중간 연산: filter, map, sorted처럼 스트림을 반환하기 때문에 여러 중간 연산을 체인(chain)처럼 연결할 수 있습니다.
  • 최종 연산: 스트림을 받아 void를 반환하거나, 리스트로의 축소(reduction) 같은 스트림이 아닌 결과값을 반환합니다.

중간 연산(Intermediate Operations)

  • 0개 이상의 중간 연산을 자유롭게 사용할 수 있습니다.
  • 연산 순서가 중요합니다. 특히 대용량 데이터셋에서는 정렬이나 매핑보다 필터를 먼저 적용하는 것이 성능에 유리합니다.
  • 매우 큰 데이터셋을 다룰 때는 ParallelStream을 사용해 멀티스레드 처리를 활성화하세요.

대표적인 중간 연산은 다음과 같습니다.

  • anyMatch()
  • distinct()
  • filter()
  • findFirst()
  • flatMap()
  • map()
  • skip()
  • sorted()

최종 연산(Terminal Operations)

최종 연산은 오직 하나만 허용됩니다.

  • forEach: 각 요소에 동일한 함수를 적용합니다. 예를 들어 모든 요소를 출력하는 경우에 사용합니다.
  • collect: 모든 요소를 컬렉션, 리스트 또는 배열에 저장합니다.
  • 기타 연산: 나머지 연산들은 스트림을 하나의 요약 값으로 축소(reduce)합니다.

대표적인 축소(reduce) 계열 함수 예시는 다음과 같습니다.

  • count()
  • max()
  • min()
  • reduce()

Java 스트림 코드 예제

이제 앞서 설명한 개념들을 실제 코드 예제를 통해 확인해 보겠습니다.

정수 스트림(Integer Stream)

첫 번째 예제는 가장 기본적인 정수 스트림입니다. IntStream 클래스의 range() 함수를 사용해 일정 범위의 정수 스트림을 생성합니다.

forEach가 최종 연산이며, 각 요소를 단순히 출력합니다.

import java.io.IOException;
import java.util.stream.IntStream;

public class JavaStreams {
    public static void main(String[] args) throws IOException {
        IntStream
            .range(1, 10)
            .forEach(System.out::print);
        System.out.println();
    }
}

실행 결과:

123456789

skip()을 활용한 정수 스트림

두 번째 예제도 정수 스트림을 사용하지만, 여기에 skip()을 추가하여 스트림의 처음 5개 요소를 건너뜁니다.

따라서 6부터 9까지의 요소만 출력되며, 출력에는 간단한 람다 표현식을 사용했습니다.

import java.io.IOException;
import java.util.stream.IntStream;

public class JavaStreams {
    public static void main(String[] args) throws IOException {
        IntStream
            .range(1, 10)
            .skip(5)
            .forEach(x -> System.out.println(x));
        System.out.println();
    }
}

실행 결과:

6
7
8
9

sum()을 활용한 정수 스트림

세 번째 예제에서는 마찬가지로 IntStream으로 스트림을 생성하지만, 이번에는 스트림 연산 전체를 println() 문의 인자로 넣습니다.

출력되는 값은 1부터 4까지(즉, 1 + 2 + 3 + 4) 숫자들의 합계입니다.

import java.io.IOException;
import java.util.stream.IntStream;

public class JavaStreams {
    public static void main(String[] args) throws IOException {
        System.out.println(
        IntStream
            .range(1, 5)
            .sum());
        System.out.println();
    }
}

실행 결과:

10

Stream.of

다음 예제는 Stream.of 함수를 사용합니다. 이 함수는 정수, 부동소수점 값, 문자열, 심지어 객체까지 스트림으로 만들 수 있어 매우 유용합니다.

이 예제에서는 문자열을 알파벳순으로 정렬한 뒤, findFirst() 함수로 첫 번째 항목을 찾아 출력합니다.

import java.io.IOException;
import java.util.stream.Stream;

public class JavaStreams {
    public static void main(String[] args) throws IOException {
        Stream.of("Ava", "Aneri", "Alberto")
            .sorted()
            .findFirst()
            .ifPresent(System.out::println);
    }
}

실행 결과:

Alberto

배열로부터 스트림 생성 후 정렬, 필터링, 출력

다음 예제는 배열로부터 스트림을 생성한 후, 정렬과 필터링을 거쳐 결과를 출력합니다.

여기서는 문자 "S"로 시작하는 항목만 필터링합니다.

각 이름을 받는 x를 인자로 하는 람다 표현식을 사용해, "S"로 시작하는 항목만 통과시킵니다. 이후 정렬하고, 통과한 각 항목을 출력합니다.

import java.io.IOException;
import java.util.Arrays;

public class JavaStreams {
    public static void main(String[] args) throws IOException {
        String[] names = {"Al", "Ankit", "Kushal", "Brent", "Sarika", "amanda", "Hans", "Shivika", "Sarah"};
        Arrays.stream(names)
                .filter(x -> x.startsWith("S"))
                .sorted()
                .forEach(System.out::println);
    }
}

실행 결과:

Sarah
Sarika
Shivika

정수 배열의 제곱 평균 구하기

이번에는 int 배열 요소들을 제곱한 값의 평균을 구하는 방법을 살펴보겠습니다.

Arrays.stream() 함수로 정수 스트림을 만든 뒤, map()을 사용해 각 정수를 제곱 값으로 변환합니다.

import java.util.Arrays;

public class JavaStreams {
    public static void main(String[] args) {
        Arrays.stream(new int[] {2, 4, 6, 8, 10})
                .map(x -> x * x)
                .average()
                .ifPresent(System.out::println);
    }
}

실행 결과:

44.0

주의할 점은 결과가 정수가 아닌 double 타입으로 출력된다는 것입니다.

리스트로부터 스트림 생성 후 필터링 및 출력

이 예제는 리스트로부터 스트림을 생성하고, 항목을 필터링한 후 출력합니다.

map() 함수 내에서 모든 이름을 소문자로 변환한다는 점에 주목하세요.

import java.util.Arrays;
import java.util.List;

public class JavaStreams {
    public static void main(String[] args) {
        List<String> people = Arrays.asList("Al", "Ankit", "Brent", "Sarika", "amanda", "Hans", "Shivika", "Sarah");
        people
                .stream()
                .map(String::toLowerCase)
                .filter(x -> x.startsWith("a"))
                .forEach(System.out::println);
    }
}

실행 결과:

al
ankit
amanda

"a"로 시작하는 세 개의 이름이 모두 소문자로 변환되어 출력된 것을 확인할 수 있습니다.

텍스트 파일의 행을 스트림으로 읽어 정렬, 필터링, 출력

다음 예제는 텍스트 파일의 각 행을 스트림으로 읽어와 정렬, 필터링, 출력을 수행합니다.

아래와 같은 내용을 담고 있는 bands.txt 파일이 있다고 가정해 보겠습니다.

Rolling Stones
Lady Gaga
Jackson Browne
Maroon 5
Arijit Singh
Elton John
John Mayer
CCR
Eagles
Pink
Aerosmith
Adele
Taylor Swift

Files.lines()를 사용해 스트림을 생성하면, 파일의 각 행이 문자열 스트림의 요소가 됩니다.

스트림을 확보한 뒤 정렬을 수행하고, 길이가 13자보다 긴 항목만 필터링하여 남은 항목들을 출력합니다.

마지막으로 파일을 닫아야 하므로 bands.close()를 호출합니다.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;

public class JavaStreams {
    public static void main(String[] args) throws IOException {
        Stream<String> bands = Files.lines(Paths.get("bands.txt"));
        bands
                .sorted()
                .filter(x -> x.length() > 13)
                .forEach(System.out::println);
        bands.close();
    }
}

실행 결과:

Jackson Browne
Rolling Stones

13자를 초과하는 두 개의 밴드 이름이 출력됩니다.

텍스트 파일의 행을 스트림으로 읽어 리스트에 저장

이 예제에서도 위와 동일한 텍스트 파일을 사용합니다.

문자열 함수인 x.contains()를 활용해 "jit"이라는 문자를 포함하는 항목만 필터링합니다.

.collect()를 사용해 "jit"을 포함하는 모든 항목을 리스트에 담습니다.

리스트가 준비되면 forEach 연산자로 각 항목을 출력할 수 있습니다.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;

public class JavaStreams {
    public static void main(String[] args) throws IOException {
        List<String> bands2 = Files.lines(Paths.get("bands.txt"))
                .filter(x -> x.contains("jit"))
                .collect(Collectors.toList());
        bands2.forEach(x -> System.out.println(x));
    }
}

실행 결과:

Arijit Singh

CSV 파일의 행을 스트림으로 읽어 개수 세기

이 예제는 CSV 파일의 행을 스트림으로 읽어온 뒤, 유효한 행의 개수를 셉니다.

다음과 같은 내용의 data.txt 파일이 있다고 가정합니다.

A,12,3.7
B,17,2.8
C,14,1.9
D,23,2.7
E
F,18,3.4

E 행에는 데이터가 없으므로, 이 행은 스트림에서 제외해야 합니다.

아래 코드에서는 각 행을 읽은 뒤 콤마(,)를 기준으로 분리해 배열로 만듭니다. 즉, 각 행이 항목 배열로 변환됩니다.

그런 다음 filter를 적용해 항목이 3개가 아닌 행을 걸러냅니다.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;

public class JavaStreams {
    public static void main(String[] args) throws IOException {
        Stream<String> rows1 = Files.lines(Paths.get("data.txt"));
        int rowCount = (int)rows1
                .map(x -> x.split(","))
                .filter(x -> x.length == 3)
                .count();
        System.out.println(rowCount + " rows.");
        rows1.close();
    }
}

실행 결과:

5 rows

축소(Reduction) - 합계 구하기

이 예제는 reduction(축소) 연산의 사용법을 보여줍니다. 여기서는 합계로 축소해 보겠습니다. Stream.of() 함수로 double 스트림을 생성하고, 세 개의 double 값을 서로 다른 인자로 전달한 후 reduce 함수를 사용합니다.

import java.util.stream.Stream;

public class JavaStreams {
    public static void main(String[] args) {
        double total = Stream.of(7.3, 1.5, 4.8)
                .reduce(0.0, (Double a, Double b) -> a + b);
        System.out.println("Total = " + total);
    }
}

실행 결과:

13.600000000000001