
저번 글에서는 함수형 인터페이스에 대해 간단히 살펴봤습니다.
이번에는 Stream에 대해 알아보겠습니다.
Stream은 데이터 컬렉션을 다루는 새로운 방식을 제공합니다.
컬렉션이나 배열을 반복문으로 직접 돌리는 대신, 선언적이고 함수형 스타일로 데이터를 처리할 수 있게 해줍니다.
Stream의 가장 큰 특징은 아래와 같습니다.
간단한 예제를 먼저 보겠습니다.
List<String> names = List.of("Alice", "Bob", "Charlie", "David");
names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.forEach(System.out::println);
// 출력: ALICE, CHARLIE, DAVID
기존에는 for문과 if문을 여러 줄 써야 했던 로직이, Stream을 사용하면 한 줄의 선언형 코드로 바뀌는 것을 확인할 수 있습니다.
Stream API는 보통 세 단계로 나눠볼 수 있습니다.
생성 (Source)
중간 연산 (Intermediate Operation)
최종 연산 (Terminal Operation)
즉, 중간 연산만 계속 정의하면 아무 일도 일어나지 않고, 최종 연산을 호출하는 순간 파이프라인이 실행됩니다.
Stream은 다양한 방법으로 만들 수 있습니다.
// 컬렉션
Stream<String> s1 = List.of("a", "b", "c").stream();
// 배열
Stream<Integer> s2 = Arrays.stream(new Integer[]{1, 2, 3});
// 값 직접
Stream<String> s3 = Stream.of("x", "y", "z");
// 무한 스트림
Stream<Integer> s4 = Stream.iterate(0, n -> n + 2);
조건에 맞는 요소만 통과시킵니다.
List<Integer> nums = List.of(1, 2, 3, 4, 5);
nums.stream()
.filter(n -> n % 2 == 0)
.forEach(System.out::println);
// 출력: 2, 4
요소를 다른 값으로 변환합니다.
List<String> names = List.of("java", "python", "go");
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
// 출력: JAVA, PYTHON, GO
요소를 정렬합니다.
List<String> names = List.of("Charlie", "Alice", "Bob");
names.stream()
.sorted()
.forEach(System.out::println);
// 출력: Alice, Bob, Charlie
Stream 결과를 다시 컬렉션으로 모읍니다.
List<String> result =
names.stream()
.filter(n -> n.length() > 3)
.collect(Collectors.toList());
각 요소를 소비합니다.
names.stream()
.forEach(System.out::println);
모든 요소를 하나로 합칩니다.
int sum = IntStream.rangeClosed(1, 5)
.reduce(0, Integer::sum);
System.out.println(sum); // 15
Stream은 간단히 병렬 처리도 지원합니다.
int total = IntStream.rangeClosed(1, 1_000_000)
.parallel()
.sum();
System.out.println(total); // 500000500000
CPU 코어를 활용하여 데이터가 많을 때 성능 이점을 볼 수 있습니다.
Stream은 컬렉션을 다루는 방식을 획기적으로 바꿔줍니다.
for문으로 반복하면서 조건문을 작성하던 방식보다 훨씬 간결하고 선언적인 코드를 작성할 수 있습니다.
특히, 중간 연산과 최종 연산의 파이프라인 구조를 이해하면 코드가 읽기 쉬워지고 유지보수도 편리해집니다.