Stream API는 컬렉션(List, Map, Set 등)의 데이터를 처리하는 데 사용되는 도구로, 데이터 변환 및 필터링 작업을 간결하고 효율적으로 수행할 수 있게 해준다.
Java8부터 도입되었으며, 병렬 처리, 함수형 프로그래밍 스타일, 데이터 스트림 조작에 초점이 맞춰져 있다.
명령형 방식
선언형 방식
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 명령형 방식으로 전부 출력
// 직관적이고, 자유도가 높다.
// 실수할 확률이 높아진다. 코드가 조금 복잡해지면 의도를 파악하기 어렵다.
System.out.println("\n=== 명령형 ===");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]);
}
// 선언형 방식으로 전부 출력
// 선언형: 뭘 하겠다고 선언 (어떻게 할지는 자바가)
// forEach: 요소들을 각각 꺼내서 동작
System.out.println("\n=== 선언형 ===");
Arrays.stream(arr).forEach(System.out::print);
// forEach 안에는 만들어져 있는 명령 => `메서드 레퍼런스`를 넣어줘야 한다.
// System.out.println("hello"); // 호출/실행
// System.out::println; // 메서드 레퍼런스
Arrays.stream(arr).forEach(Main::test);
// 1부터 100까지 짝수만 출력하기
// 숫자 스트림을 바로 만들 수 있음
IntStream.rangeClosed(1, 100).forEach(Main::even);
// 람다 -> 익명함수
// 함수를 정의하지 않고, 일회용으로 사용할 수 있다.
// (`매개변수`) -> {`함수`}
IntStream.rangeClosed(1, 100).forEach((num) -> {
if (num % 2 == 0) {
System.out.println(num);
}
});
// 기본은 람다를 쓰되, 람다 안에서 사용하는게 재사용성이 높거나 가독성이 떨어지는 복잡한 코드일 때
// -> 함수 정의
}
public static void test(int num) {
System.out.print("\n숫자 : " + num);
}
public static void even(int num) {
if (num % 2 == 0) {
System.out.println(num);
}
}
}
Source → 중간 연산 → 최종 연산
List list = Arrays.asList(1, 2, 3); Stream stream = list.stream();
filter(), map(), sorted()
forEach(), collect(), count()
list.stream().filter(x -> x > 2).forEach(System.out::println);
list.stream().map(x -> x * 2).forEach(System.out::println);
list.stream().sorted().forEach(System.out::println);
list.stream().distinct().forEach(System.out::println);
List<Integer> result = list.stream().filter(x -> x > 2).collect(Collectors.toList());
list.stream().forEach(System.out::println);
long count = list.stream().filter(x -> x > 2).count();
int sum = list.stream().reduce(0, Integer::sum);
연습하기
public class Main {
public static void main(String[] args) {
String[] strNums = {"1번", "2번", "3번", "4번", "5번"};
// 홀수만 뽑아서 int 배열 만들기
int[] numbers = Arrays.stream(strNums)
.map(str -> str.replace("번", ""))
.mapToInt(str -> Integer.parseInt(str))
.filter(num -> num % 2 == 1)
.toArray();
Arrays.stream(numbers).forEach(System.out::println);
}
}
데이터를 병렬로 처리하는 기능. 데이터를 여러 스레드로 분할하여 동시에 처리함으로써 처리 속도를 향상시킨다. 일반적인 순차 스트림은 한 번에 한 요소씩 처리하지만, 병렬 스트림은 데이터 소스를 여러 청크로 나누어 병렬로 처리한다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); numbers.stream().forEach(System.out::println);
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); numbers.parallelStream().forEach(System.out::println);
병렬 스트림의 동작 원리
병렬 스트림이 적합한 경우
병렬 스트림을 사용하지만 출력의 순서를 보장해야 하는 경우
names.parallelStream().forEachOrdered(System.out::println);
public class Main {
public static void main(String[] args) {
long n = 10_000_000;
// 순차 스트림
long start = System.currentTimeMillis();
LongStream.range(1, n)
.forEachOrdered(x -> {}); // 반복
long end = System.currentTimeMillis();
System.out.println("순차 스트림 : " + (end - start) + "ms");
// 병렬 스트림
start = System.currentTimeMillis();
LongStream.range(1, n)
.parallel()
.forEachOrdered(x -> {}); // 반복
end = System.currentTimeMillis();
System.out.println("병렬 스트림 : " + (end - start) + "ms");
}
}
실행 결과
순차 스트림 : 6ms
병렬 스트림 : 29ms
import java.util.stream.LongStream;
public class Main {
public static void main(String[] args) {
long n = 10_000; // 작은 데이터
// 명령형 방식
long start = System.nanoTime();
long sumImperative = imperativeSum(n);
long end = System.nanoTime();
System.out.println("명령형 방식 합계: " + sumImperative);
System.out.println("명령형 방식 시간: " + (end - start) + "ns");
// 순차 스트림
start = System.nanoTime();
long sumSequential = sequentialStreamSum(n);
end = System.nanoTime();
System.out.println("순차 스트림 합계: " + sumSequential);
System.out.println("순차 스트림 시간: " + (end - start) + "ns");
// 병렬 스트림
start = System.nanoTime();
long sumParallel = parallelStreamSum(n);
end = System.nanoTime();
System.out.println("병렬 스트림 합계: " + sumParallel);
System.out.println("병렬 스트림 시간: " + (end - start) + "ns");
}
public static long imperativeSum(long n) {
long sum = 0;
for (long i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
public static long sequentialStreamSum(long n) {
return LongStream.rangeClosed(1, n).sum();
}
public static long parallelStreamSum(long n) {
return LongStream.rangeClosed(1, n).parallel().sum();
}
}
실행 결과
명령형 방식 합계: 50005000
명령형 방식 시간: 108292ns
순차 스트림 합계: 50005000
순차 스트림 시간: 966209ns
병렬 스트림 합계: 50005000
병렬 스트림 시간: 1514208ns
import java.util.stream.LongStream;
public class StreamPerformanceComparison {
public static void main(String[] args) {
long n = 100_000_000L; // 큰 데이터
// 명령형 방식
long start = System.currentTimeMillis();
long sumImperative = imperativeSum(n);
long end = System.currentTimeMillis();
System.out.println("명령형 방식 합계: " + sumImperative);
System.out.println("명령형 방식 시간: " + (end - start) + "ms");
// 순차 스트림
start = System.currentTimeMillis();
long sumSequential = sequentialStreamSum(n);
end = System.currentTimeMillis();
System.out.println("순차 스트림 합계: " + sumSequential);
System.out.println("순차 스트림 시간: " + (end - start) + "ms");
// 병렬 스트림
start = System.currentTimeMillis();
long sumParallel = parallelStreamSum(n);
end = System.currentTimeMillis();
System.out.println("병렬 스트림 합계: " + sumParallel);
System.out.println("병렬 스트림 시간: " + (end - start) + "ms");
}
public static long imperativeSum(long n) {
long sum = 0;
for (long i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
public static long sequentialStreamSum(long n) {
return LongStream.rangeClosed(1, n).sum();
}
public static long parallelStreamSum(long n) {
return LongStream.rangeClosed(1, n).parallel().sum();
}
}
실행 결과
명령형 방식 합계: 5000000050000000
명령형 방식 시간: 32ms
순차 스트림 합계: 5000000050000000
순차 스트림 시간: 36ms
병렬 스트림 합계: 5000000050000000
병렬 스트림 시간: 9ms