프로그래밍 공부
[자바] #21. 스트림(Stream) - 2편 (병렬 스트림, 중간 연산 추가 내용) 본문
📚 병렬 스트림
스트림의 요소들을 여러 스레드에서 병렬로 처리하여 성능을 향상시키는 스트림.
내부적으로 'ForkJoinPool'을 사용하여 작업을 분할하고 병렬로 실행한다.
대용량 데이터 처리나 계산 집약적인 작업에서 CPU 활용도를 높여 처리 속도를 개선할 수 있다.
⚡ 병렬 스트림 생성 방법 1, 2
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 기존 스트림을 병렬 스트림으로 변환
numbers.stream().parallel().forEach(System.out::println);
// 컬렉션에서 바로 병렬 스트림 생성
numbers.parallelStream().forEach(System.out::println);
🚀 병렬 스트림과 일반 스트림 차이
| 일반 스트림 (stream()) | 병렬 스트림 (parallelStream()) |
| 단일 스레드에서 순차적으로 처리 | 멀티 스레드를 사용하여 병렬 처리 |
| 순서 보장 (forEachOrdered 제외 시) | 처리 순서가 보장되지 않음 (forEachOrdered 사용 시 보장 가능) |
| 대용량 데이터 처리 시 상대적으로 느림 | 대용량 데이터 처리 시 빠름 (CPU 코어 활용) |
| CPU 활용도가 낮음 | CPU 멀티코어를 활용하여 성능 최적화 |
📝 병렬 스트림 예제 - 대용량 데이터 처리 시간 비교
import java.util.*;
import java.util.stream.*;
import java.time.*;
public class ParallelStreamExample {
public static void main(String[] args) {
List<Integer> numbers = IntStream.rangeClosed(1, 1000000)
.boxed()
.collect(Collectors.toList());
// 일반 스트림 처리 시간
Instant start = Instant.now();
numbers.stream().reduce(0, Integer::sum);
Instant end = Instant.now();
System.out.println("일반 스트림 처리 시간: " + Duration.between(start, end).toMillis() + "ms");
// 병렬 스트림 처리 시간
start = Instant.now();
numbers.parallelStream().reduce(0, Integer::sum);
end = Instant.now();
System.out.println("병렬 스트림 처리 시간: " + Duration.between(start, end).toMillis() + "ms");
}
}
일반 스트림 처리 시간: 120ms
병렬 스트림 처리 시간: 40ms
⚡ 병렬 스트림이 대용량 데이터 처리 시 더 빠른 속도를 보여준다.
예제에서 numbers 스트림을 만드는 과정 해석:
1️⃣ IntStream.rangeClosed(1, 1000000)
- IntStream은 기본형 스트림(int)을 다루는 스트림
- rangeClosed(start, end) 메서드는 start부터 end까지 (양쪽 모두 포함)의 정수를 생성한다. (예를 들어 range(1, 5)는 1, 2, 3, 4, 5를 생성한다)
2️⃣ boxed()
- 기본형 스트림(IntStream)을 참조형 스트림(Stream<Integer>)으로 변환
- 즉, int → Integer로 변환하는 오토 박싱 과정을 수행한다.
💡 왜 필요할까?
- 기본형 스트림(IntStream)은 성능상 유리하지만, 객체(Integer)를 요구하는 연산에서는 사용하기 어렵다.
- 예를 들어, List<Integer>와 같은 컬렉션으로 수집하려면 객체 스트림(Stream<Integer>)이어야 한다.
IntStream.rangeClosed(1, 3)
.boxed()
.forEach(System.out::println);
⚡ 요약:
- int → Integer로 박싱
- IntStream → Stream<Integer>로 변환
- 이후 객체 스트림이 필요한 연산(collect, map, filter) 등에 사용할 수 있음
3️⃣ collect(Collectors.toList())
- 스트림의 요소들을 List 컬렉션으로 수집
- collect() 메서드는 스트림의 최종 연산 중 하나이며, 스트림을 특정한 형태로 수집하는 데 사용된다.
collect(Collector<T, A, R> collector) 형태로 사용되며,
Collectors.toList()는 스트림의 모든 요소를 List에 담아 반환한다.
✅ 다른 수집 예시:
| 메서드 | 설명 | 예시 출력 |
| Collectors.toList() | 요소를 List로 수집 | [1, 2, 3] |
| Collectors.toSet() | 요소를 Set으로 수집 (중복 제거) | [1, 2, 3] |
| Collectors.joining(",") | 문자열 스트림을 연결 (구분자 추가 가능) | "1,2,3" |
| Collectors.toMap(...) | 요소를 Map으로 수집 | {key1=value1, ...} |
🎯 최종 요약: 전체 코드 동작 과정
List<Integer> numbers = IntStream.rangeClosed(1, 1000000) // 1부터 1,000,000까지 정수 생성 (기본형 int 스트림)
.boxed() // int → Integer 변환 (Stream<Integer>로 변환)
.collect(Collectors.toList()); // Stream<Integer>를 List<Integer>로 수집
🧩 중간 연산 추가 내용들
1️⃣ flatMap()
각 요소를 스트림으로 변환한 후, 중첩된 스트림을 하나의 스트림으로 평면화(flatten)
<R> Stream<R> flatMap(Function<T, Stream<R>> mapper)
// 원래는 <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
// map 메서드는 아래와 같이 생겼다.
<R> Stream<R> map(Function<T, R> mapper)
Function<T, R>의 추상 메서드는 R apply(T t)이므로, 위 메서드 호출 시 전달해야 할 메서드는 다음과 같다:
Stream<R> apply(T t)
즉 flatMap에 전달할 람다식에서는, '스트림을 생성하고 이를 반환'해야 한다.
반면 map에 전달할 람다식에서는 스트림을 구성할 데이터만 반환하면 되는 것이다.
💡 예제 1: 문자열을 단어로 분해하여 평면화하기
import java.util.*;
import java.util.stream.*;
public class FlatMapExample {
public static void main(String[] args) {
List<String> sentences = Arrays.asList("Java is fun", "Streams are powerful");
List<String> words = sentences.stream()
.flatMap(sentence -> Arrays.stream(sentence.split(" "))) // 문자열 분해 후 평면화
.collect(Collectors.toList());
System.out.println(words); // 출력: [Java, is, fun, Streams, are, powerful]
}
}
💡 예제 2: 리스트의 리스트 평면화하기
import java.util.*;
import java.util.stream.*;
public class FlatMapListExample {
public static void main(String[] args) {
List<List<Integer>> listOfLists = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9)
);
List<Integer> flatList = listOfLists.stream()
.flatMap(Collection::stream) // 리스트를 평면화
.collect(Collectors.toList());
System.out.println(flatList); // 출력: [1, 2, 3, 4, 5, 6, 7, 8, 9]
}
}
📝 예제 2 설명:
1️⃣ listOfLists.stream():
- List<List<Integer>>를 스트림으로 변환 (Stream<List<Integer>>)
- 형태: [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]
2️⃣ flatMap(Collection::stream):
- 각 내부 리스트(List<Integer>)를 스트림(Stream<Integer>)으로 변환
- 결과적으로 Stream<Stream<Integer>> 형태가 되지만, flatMap()이 각 내부 스트림을 평면화하여
👉 Stream<Integer> 형태로 반환한다. - 형태: [1, 2, 3, 4, 5, 6, 7, 8, 9]
3️⃣ collect(Collectors.toList()):
- 평면화된 스트림을 List<Integer>로 수집
⚡ flatMap vs map 차이점
List<Stream<Integer>> mappedList = listOfLists.stream()
.map(Collection::stream)
.collect(Collectors.toList());
[java.util.stream.ReferencePipeline$Head@...,
java.util.stream.ReferencePipeline$Head@...,
java.util.stream.ReferencePipeline$Head@...]
🔥 왜 이렇게 출력될까?
- map()은 각 내부 리스트를 스트림 자체로 변환하지만,
- 평면화하지 않기 때문에 스트림의 스트림(Stream<Stream<Integer>>) 형태가 된다.
- 즉, 내부 스트림을 단일 스트림으로 합치지 않고 그대로 유지한다.
🌈 flatMap()의 내부 동작 시각화
// 입력 데이터 (중첩 리스트)
[
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
// ✅ flatMap(Collection::stream) 처리 과정:
Stream [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 🤔 map(Collection::stream) 처리 과정:
Stream [
Stream [1, 2, 3],
Stream [4, 5, 6],
Stream [7, 8, 9]
]
2️⃣ sorted() — 정렬(Sorting)
Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)
스트림의 요소를 정렬.
기본적으로 자연 순서(Comparable)에 따라 정렬하지만, Comparator를 전달하여 사용자 정의 정렬도 가능하다.
💡 예제 1: 기본 정렬 (오름차순)
import java.util.*;
import java.util.stream.*;
public class SortedExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 3);
List<Integer> sortedNumbers = numbers.stream()
.sorted() // 오름차순 정렬
.collect(Collectors.toList());
System.out.println(sortedNumbers); // 출력: [1, 2, 3, 5, 8]
}
}
💡 예제 2: 사용자 정의 정렬 (내림차순)
import java.util.*;
import java.util.stream.*;
public class CustomSortedExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
List<String> sortedNames = names.stream()
.sorted(Comparator.reverseOrder()) // 내림차순 정렬
.collect(Collectors.toList());
System.out.println(sortedNames); // 출력: [Charlie, Bob, Alice]
}
}
여기서 Comparator.reverseOrder() 란:
기본 정렬 순서를 반대로(내림차순으로) 만드는 Comparator를 반환하는 정적 메서드.
3️️ peek() — 루핑(디버깅용)
Stream<T> peek(Consumer<? super T> action)
스트림의 요소를 소비하지 않고 중간 과정에서 엿보기(peek) 위한 메서드.
주로 디버깅이나 중간 단계 확인을 위해 사용된다.
forEach()와 달리 중간 연산이며, 최종 연산이 호출될 때까지 실행되지 않는다.
💡 예제: peek()를 사용하여 디버깅
import java.util.*;
import java.util.stream.*;
public class PeekExample {
public static void main(String[] args) {
List<String> result = Stream.of("one", "two", "three", "four")
.filter(s -> s.length() > 3)
.peek(s -> System.out.println("Filtered value: " + s)) // 중간 과정 출력
.map(String::toUpperCase)
.peek(s -> System.out.println("Mapped value: " + s)) // 변환 과정 출력
.collect(Collectors.toList());
System.out.println("최종 결과: " + result);
}
}
'자바' 카테고리의 다른 글
| [자바] #23. 날짜/시간 관련 클래스들 (1) | 2025.02.23 |
|---|---|
| [자바] #22. 스트림(Stream) - 3편 (최종 연산 추가 내용) (0) | 2025.02.21 |
| [자바] #20. 스트림(Stream) - 1편 (+ 오토 박싱 / 언박싱) (1) | 2025.02.21 |
| [자바] #19. Optional 총정리 (0) | 2025.02.20 |
| [자바] #18. 메서드 참조(+ 람다식에서 접근 가능한 참조변수 제한) (0) | 2025.02.20 |