이 글에서는 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