728x90
Stream
@2024.04.25
스트림(Stream)
💡
자바 8에서 도입된 개념이며, 데이터의 추상화된 연속된 흐름을 의미하며, 데이터 컬렉션을 더 간결하고 직관적인 방식으로 처리할 수 있게 해준다.
- 주요 특징
- 더 나은 데이터 처리
- 데이터를 필터링하거나 변환하는 작업이 단순화되어, 코드가 더 간결하고 읽기 쉽다.
- 함수형 프로그래밍 스타일 지원
- 자바는 전통적으로 명령형 프로그래밍 언어
- 스트림을 통해 함수형 프로그래밍 사용 가능
- 병렬 처리 지원
- 멀티 코어 프로세서의 장점을 활용하여 데이터 처리 성능을 향상
- paralleStream()를 통해 간단하게 병렬 처리가 가능
- 더 나은 데이터 처리
- 컬렉션 vs 스트림
- 데이터 처리 방식
- 컬렉션
- 데이터를 저장하고 관리하는 구조
- 컬렉션에 데이터를 저장하면 모든 요소가 메모리에 저장
- 데이터를 추가, 삭제, 검색하는 등의 작업 수행
- 스트림
- 데이터의 흐름
- 데이터를 저장하지 않고 필요에 따라 요소를 계산 (=지연 계산)
- 데이터가 필요할 때만 처리되고, 그 이전에는 실행되지 않는다.
- 컬렉션
- 사용 목적과 특성
- 컬렉션
- 데이터의 전체적인 관리와 접근에 중점
- 스트림
- 데이터의 변환과 처리에 중점
- 데이터를 필터링하거나, 매핑하는 등의 연산에 최적화
- 컬렉션
- 병렬 처리
- 컬렉션
- 병렬 처리를 직접 구현해야 한다.
- Collections.synchronizedList()나 ConcurrentHashMap과 같은 동시성 컬렉션 사용
- 스트림
- paralleStream()를 사용하여 내장된 병렬 처리 기능 제공
- 컬렉션
- 데이터 처리의 효율성
- 컬렉션
- 대량의 데이터를 처리할 때, 전체 데이터가 메모리에 올라가야 하므로 메모리 사용량이 높다.
- 스트림
- 지연 계산 방식을 사용하기에 필요한 데이터만 처리하고 나머지는 무시할 수 있어 메모리 효율성이 더 좋다.
- 컬렉션
- 데이터 처리 방식
스트림 생성과 활용
💡
스트림은 다양한 데이터 소스로부터 생성
- 주요 생성 방법
- 컬렉션에서 스트림 생성
- 대부분의 컬렉션 클래스는 stream() 제공
List<String> myList = Arrays.asList("a", "b", "c"); Stream<String> myStream = myList.stream();
- 배열에서 스트림 생성
- 배열로부터 스트림을 생성하기 위해 Arrays.stream()을 사용
String[] myArray = new String[]{"a", "b", "c"}; Stream<String> arrayStream = Arrays.stream(myArray);
- 스트림의 정적 메소드 사용
- of()
- 주어진 요소로 스트림을 생성
- iterate()
- 초깃값과 함수를 사용하여 스트림을 생성
- generate()
- Supplier를 사용하여 스트림을 생성
Stream<Integer> number = Stream.of(1, 2, 3); Integer[] numbers = {1, 2, 3, 4, 5, 1, 2, 3}; Stream.of(numbers) // 1부터 시작해서 2씩 증가하는 무한 스트림 생성 Stream<Integer> numbers = Stream.iterate(1, n -> n + 2) // 무한한 난수를 생성하는 스트림 생성 Stream<Integer> randomNumbers = Stream.generate(() -> new Random().nextInt(100));
- of()
- 파일로부터 스트림 생성
- java.nio.file.Files 클래스를 사용하면 파일의 각 라인을 스트림 요소로 변환 가능
Path path = Paths.get("src/com/example/"); Stream<Path> stream = Files.list(path); stream.forEach(p -> System.out.println(p.getFileName())); stream.close(); Stream<String> stream2 = Files.lines(Paths.get("src/com/example/test.java")); stream2.forEach(System.out::println); stream2.close();
- 빌더를 사용한 스트림 생성
- Stream.builder()를 사용하여 스트림을 수동으로 구성 가능
- 동적으로 스트림을 생성할 때 유용
Stream<String> builtStream = Stream.<String>builder().add("a).add("b").build();
- 컬렉션에서 스트림 생성
스트림 API의 연산
💡
스트림 API는 데이터 처리 파이프라인을 구성하기 위해 다양한 연산을 제공하며,
이 연산은 크게
중간 연산과 최종 연산으로 나뉜다.
이 연산은 크게
중간 연산과 최종 연산으로 나뉜다.

중간 연산(Middle Operations)🎀
💡
스트림을 다른 스트림으로 변환하는 연산으로, 여러 개의 중간 연산이 연결될 수 있다.
중간 연산은
리턴 값을 스트림으로 받는다.
중간 연산은
리턴 값을 스트림으로 받는다.
- 주요 특징
- 연쇄 가능
- 여러 중간 연산을 연결하여 복잡한 데이터 처리 파이프라인을 구성
- 지연 실행
- 중간 연산은 최종 연산이 호출될 때까지 실제로 실행되지 않는다.
- 데이터 처리의 효율성을 높인다.
- 상태 없음 혹은 상태 보유
- 일부 중간 연산은 상태를 유지하나, 정렬(sort) 또는 중복 제거(distinct)와 같이 상태를 유지하는 연산도 존재
- 연쇄 가능
최종 연산(Terminal Operations)
💡
스트림의 요소를 소모하여 결과를 도출하는연산으로, 리턴 값을 최종 값으로 받는다.
- 최종 연산이 호출되면 스트림 파이프라인이 실행되며, 이후 스트림은 사용할 수 없다.
- 주요 특징
- 스트림 소비
- 스트림의 요소를 소비하여, 단일 값이나 컬렉션 혹은 아무것도 반환하지 않을 수 있다.
- 스트림 파이프라인의 실행
- 중간 연산들이 처리된 데이터에 적용되며, 그 결과를 반환한다.
- 스트림 소비
- 최종 연산의 예
- forEach
- 각 요소를 반복하면서 주어진 작업을 수행하는 메서드
- filter 메서드는 arr 배열에서 짝수만 필터링하고, forEach는 그 결과를 출력한다.
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; Arrays.stream(arr) // 스트림 생성 .filter(i -> i % 2 == 0) // 스트림에서 짝수만 필터링 //필터링된 요소들을 하나씩 꺼내서 System.out.println을 사용하여 출력 .forEach(System.out::println); // 2 4 6 8 10 //스트림을 사용 하지 않았다면? for (Integer i : arr) { if (i % 2 == 0) { System.out.println(i); } }
- collect
- 스트림의 요소들을 수집하여 다른 형태의 데이터 구조로 변환하거나,
요소들을 그룹화하거나, reduce 연산 등을 수행하는 메서드
- Collector 인터페이스를 구현한 객체를 인자로 받으며, 이 객체는 수집 과정을 정의한다.
- 주로 사용되는 Collector는 Collectors 유틸리티 클래스에서 제공된다.
- filter 메서드는 사로 시작하는 문자열을 선택하고, collect는 그 결과를 리스트로 수집
List<String> myList = Arrays.asList("사과", "포도", "파인애플", "감"); List<String> filteredList = myList.Stream // 스트림 생성 .filter(s -> s.startsWith("사")) // 중간 연산 .collect(Collectors.toList()); // 최종 연산 [사과] // 스트림을 쓰지 않고 코드를 만든다면? List<String> filteredList1 = new ArrayList<>(); for (String str : myList) { if (str.startsWith("사")) filteredList1.add(str); } System.out.println(filteredList1);
- 스트림의 요소들을 수집하여 다른 형태의 데이터 구조로 변환하거나,
- reduce
- 배열의 요소를 합치거나 최댓값 또는 최솟값을 찾는 등의 작업을 수행하는 메서드
- 첫 번째 매개변수는 초기 값, 두 번째 매개변수는 누적 값, 세 번째 매개변수는 현재 값
- 배열의 요소를 모두 더하는 예시
int[] arr = {1, 2, 3, 4, 5}; // 배열의 요소들을 모두 더하기 int sum = Arrays.stream(arr) // 스트림 생성 .reduce(0, (acc, x) -> acc + x); //0 : 초기 값, acc : 누적 값, x : 현재 값 System.out.println("배열의 요소들의 합: " + sum); // 15
- match
- 스트림의 요소들이 주어진 조건과 일치하는지를 확인하여 boolean을 반환하는 메서드
- allMatch() : 스트림의 모든 요소가 주어진 조건을 만족하는가?
- anyMatch() : 스트림의 요소 중 하나 이상이 주어진 조건을 만족하는가?
- noneMatch() : 스트림의 모든 요소가 주어진 조건을 만족 못 하는가?
int[] arr = {1, 2, 3, 4, 5}; // 배열의 요소 중에서 3이 있는지 확인 boolean result = Arrays.stream(arr) // 스트림 생성 .anyMatch(x -> x == 3); if (result) { System.out.println("배열에 3이 포함되어 있습니다."); } else { System.out.println("배열에 3이 포함되어 있지 않습니다."); }
- 스트림의 요소들이 주어진 조건과 일치하는지를 확인하여 boolean을 반환하는 메서드
- forEach
필터링과 변환(fillter/distinct)
💡
데이터 필터링은 스트림에서 특정 조건에 맞는 요소만 선택하는 과정으로,
불필요한 데이터를 제거하고, 필요한 데이터만을 남긴다.
불필요한 데이터를 제거하고, 필요한 데이터만을 남긴다.
- 데이터 분석, 웹 서비스의 요청 처리, 파일 처리 등의 다양한 분야에서 활용
- 대용량 데이터 처리 시, 불필요한 데이터를 제거함으로써 메모리 사용량을 줄인다.
- 예시
- 글자 수가 3개 이상인 과일들을 선택하고 중복을 제거한 후, 결과를 리스트로 수집
List<String> words = Arrays.asList("사과", "바나나", "체리", "사과", "체리", "포도"); List<String> fruits = words.stream() // 스트림 변환 .filter(fruit -> fruit.length() > 3) // 조건을 통해 필터링 .distinct() // 중복 제거 .collect(Collectors.toList()); // 결과 수집 System.out.println(fruits); // [바나나, 체리, 포도] // 스트림을 사용하지 않고 코드를 만든다면? List<String> filteredWords1 = new ArrayList<>(); for (String w : words) { if (w.length() > 3) { if (filteredWords1.contains(w)) continue; else filteredWords1.add(w); } } System.out.println(filteredWords1);
filter 메서드🎀
💡
주어진 조건에 맞는 요소만을 스트림에서 추출
- 사용 방법
- Predicate 인터페이스를 파라미터로 받는다.
- 이를 활용해 스트림의 각 요소가 주어진 조건에 부합하는지 검사
- 예시
String[] arr = {"Apple", "Banana", "Orange", "Grape", "Pineapple"}; // 주어진 문자열을 포함하는 요소만 필터링하여 출력 Stream.of(arr) // arr 배열을 스트림으로 생성 .filter(element -> element.contains("le")) // le 포함하는 요소만 필터링 .forEach(System.out::println); // Apple Pineapple
distinct 메서드🎀
💡
스트림에서 중복을 제거하며, 주로 fillter()와 함께 사용되어, 중복되지 않는 요소만을 반환
- equals 메서드를 기반으로 요소의 동등성을 판단
- 필요시 해당 메서드를 재정의할 필요 존재
- 예시
Integer[] numbers = {1, 2, 3, 4, 5, 1, 2, 3}; // 중복된 숫자를 제거하여 출력 Stream.of(numbers) .distinct() .forEach(System.out::println); // 1 2 3 4 5 }
데이터 변환(map/flatMap)
💡
스트림의 각 요소를 원하는 형태로 변환하는 과정으로, 데이터를 가공하고, 정보를 추출하는데 필수적인 작업
- JSON 데이터 처리, 파일 데이터 가공, 데이터베이스 쿼리 결과 등의 변환 등에 주로 사용
- 복잡한 데이터 구조를 간단하게 하고 명확한 구조로 변환하는데 필요
List<List<String>> fruits = Arrays.aslist( Arrays.asList("사과", "바나나"), Arrays.asList("체리", "포도") ); List<String> arrList = fruits.stream() // 스트림 생성 .flatMap(Collection::stream) // 스트림 평탄화 .collect(Collectors.toList()); // 결과 수집 System.out.println(arrList); // [사과, 바나나, 체리, 포도]
map 메소드 🎀
💡
스트림의 각 요소를 주어진 함수에 따라 다른 형태로 변환
- 사용 방법
- Function 인터페이스를 인자로 받는다.
- 각 요소를 다른 형태로 매핑하는 역할
- 예시
String[] words = {"apple", "banana", "cherry", "grape"}; // 각 문자열을 대문자로 변환하여 출력 Stream<String> upperCaseStream = Arrays.stream(words) .map(element -> element.toUpperCase()); // 변환된 문자열 출력 upperCaseStream.forEach(System.out::println); // APPLE BANANA CHERRY GRAPE
flatmap 메소드 🎀
💡
각 요소를 스트림으로 변환하고, 그 스트림들을 하나의 스트림으로 합친다.
- 중첩된 구조를 통합할 때 사용된다.
- 사용 방법
- Function 인터페이스를 인자로 받는다.
- 각 요소를 스트림으로 변환한다.
- 예시
List<List<String>> listOfLists = Arrays.asList( Arrays.asList("apple", "banana"), Arrays.asList("cherry", "grape"), Arrays.asList("orange", "kiwi") ); // 리스트 안의 리스트를 평탄화하여 하나의 스트림으로 생성 Stream<String> flatMapStream = listOfLists.stream() .flatMap(list -> list.stream()); // 평탄화된 스트림을 출력 flatMapStream.forEach(System.out::println); // apple banana cherry grape orange kiwi
List<String> list2 = Arrays.asList("1 ,3, 4,6 ,7 ,8, 9"); list2.stream() .flatMapToInt( data -> { String[] strArr = data.split(","); int[] intArr = new int[strArr.length]; for (int i = 0; i < strArr.length; i++) { intArr[i] = Integer.parseInt(strArr[i].trim()); } return Arrays.stream(intArr); }) .forEach(System.out::print); "==============================================================================" list2.stream().flatMapToInt(data -> Arrays.stream(data.split(",")) .mapToInt(str -> Integer.parseInt(str.trim()))) .forEach(System.out::print);
요소 정렬(sorted)
💡
데이터를 특정 순서로 정렬한다.
- 데이터를 가공하고, 분석 하기 전에 중요한 전처리 과정
- DB 쿼리 결과의 정렬, 파일에서 읽은 데이터의 정렬, 실시간 데이터 스트림의 정렬 등에 사용
sorted 🎀
💡
스트림의 요소들이 주어진 기준에 따라 정렬
- 기본 정렬
- sorted() 메서드
- 오름차순으로 데이터를 정렬
- 요소들이 Comparable 인터페이스를 구현했을 때 사용 가능
import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; class Student implements Comparable<Student> { private String name; private int age; public Student(String name, int age){ this.name = name; this.age = age; } public String getName(){ return name; } public int getAge(){ return age; } @Override public int compareTo(Student o) { return 0; } } public class practice_03 { public static void main(String[] args) { List<Student> students = Arrays.asList( new Student("Alice", 82), new Student("Bob", 90), new Student("Charlie", 72), new Student("David", 76) ); List<Student> grade = students.stream() .filter(x -> x.getAge() >= 80) .sorted() .collect(Collectors.toList()); grade.forEach(student -> System.out.println(student.getName())); } }
- 사용자 지정 정렬
- sorted(Comparator<T>) 메서드
- 내림 차순 정렬, 특정 속성 기준 정렬 등을 수행
- 예시
- 기본 정렬 : stream.sorted() - 오름 차순 정렬
- 사용자 정의 정렬 : stream.sorted(Comparator.reverseOrder() - 내림차순 정렬
List<String> fruits = Arrays.asList("Banana", "Apple", "Cherry", "Orange"); // 오름차순 정렬 List<String> sortedFruits = fruits.stream() .sorted() .collect(Collectors.toList()); // 내림차순 정렬 List<String> reverseSortedFruits = fruits.stream() .sorted(Comparator.reverseOrder()) .collect(Collectors.toList());
요소 순회(forEach, peek)
💡
순회를 통해 각 요소에 대한 연산을 실행하고, 데이터를 조작하거나, 결과를 출력할 수 있다.
forEach
💡
스트림의 각 요소에 대해 주어진 작업을 수행
- 최종 연산으로, 스트림의 모든 요소에 대해 주어진 작업을 실행하고 스크림 처리를 종료
- 데이터 출력, 요소에 대한 조작 등에 사용
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); numbers.stream() .forEach(n -> System.out.println("Number : " + n);
peek🎀
💡
스트림의 각 요소에 대해 작업을 수행하나, 스트림 자체는 변화시키지 않는다.
- 중간 연산으로, 디버깅이나 요소에 대한 임시 처리를 위해 사용
- forEach와 달리 peek는 스트림의 흐름을 중단하지 않는다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Interger> doubleNumbers = numbers.stream() .peek(n -> System.out.println("Processing 1 : " + n)) .map(n -> n*2) .peek(n -> System.out.println("Processing 1 : " + n)) .collect(Collectors.toList());
스트림 요소 집계 연산 : count, max, min, average, sum
💡
특정 기준에 따라 데이터를 요약하고 분석하는 데 사용
- 대규모 데이터 셋에서 유용한 정보를 추출하고 데이터의 특성을 파악 가능
- 데이터 분석, 보고서 생성, 의사 결정 지원 등 다양한 분야에서 사용
count
💡
스트림 내 요소의 개수 반환
- 예시
- 데이터 셋 내 특정 조건을 만족하는 요소의 수 세기
List<Integer> numbers = Array.asList(); // List가 비어 있으면 0 반환 long count = numbers.stream() .count();
max / min
💡
스트림 내 최대/최솟값 찾기
- 데이터 중 가장 크거나 작은 값을 찾을 때 사용
- 예시
- 최고/최저 점수 찾기
- orElse() : Java Optional 중 하나로 객체가 비어있을 때 대체 값을 제공하는 데 사용
List<Integer> numbers = Array.asList(); // List가 비어 있으면 0 반환 int max = number.stream() .max(Interger::compareTo).orElse(0); int min = number.stream() .min(Interger::compareTo).orElse(0);
average
💡
스트림 내 요소들의 평균 값을 계산
- 데이터의 평균적인 특성을 파악할 때 사용
- 예시
- 평균 점수나 평균 가격 구하기
List<Integer> numbers = Array.asList(); // List가 비어 있으면 0.0 반환 double average = number.stream() .mapToInt(Integer::intValue) .average() .orElse(0.0);
sum
💡
스트림 내의 요소들의 합을 계산
- 데이터의 총합이 필요할 때 사용된다.
- 예시
- 총매출액이나 총 점수 계산하기
List<Integer> numbers = Array.asList(); int sum = numbers.stream(0 .mapToInt(Integer::intValue) .sum();
스트림 결과 수집 : Collectors 클래스
💡
스트림의 요소들을 다양한 형태의 결과로 수집하는 데 사용되는 메서드를 제공
- Collectors 클래스 주요 기능
- 목록, 세트, 맵 수집
- toList(), toSet(), toMap() 메서드
- 스트림의 요소들을 List, Set, Map과 같은 컬렉션으로 수집
// Person 객체의 스트림에서 ㄱ이름을 추출하여 리스트로 수집 List<String> names = people.stream() .map(Person::getName) .collect(Collectors.toList());
- toList(), toSet(), toMap() 메서드
- 데이터 요약
- sumarizingInt(), sumarizingDouble(), sumarizingLong() 메서드
- 스트림의 숫자 데이터를 요약
// 스트림에서 요소의 통계 정보를 계산 IntSummaryStatistics stats = numbers.stream() .collect(Collectors.summarizingInt(Integer::intValue)); // 통계 정보 출력 System.out.println("합계: " + stats.getSum()); System.out.println("최소값: " + stats.getMin()); System.out.println("최대값: " + stats.getMax()); System.out.println("평균: " + stats.getAverage()); System.out.println("개수: " + stats.getCount());
- sumarizingInt(), sumarizingDouble(), sumarizingLong() 메서드
- 문자열 결합
- joining() 메서드
- 스트림의 문자열 요소들을 하나의 문자열로 결합
- 구분자, 접두사, 접미사를 선택적으로 사용할 수 있다.
List<String> words = Arrays.asList("Java", "is", "awesome"); // 리스트의 요소를 하나의 문자열로 결합 String result = words.stream() .collect(Collectors.joining(" ")); System.out.println(result); //Java is awesome
- joining() 메서드
- 그룹화와 분할
- groupinBy()
- 스트림 내의 요소들을 특정 기준에 따라 분리하는 방식
List<String> words = Arrays.asList("apple", "banana", "orange", "kiwi", "grape"); // 문자열을 길이를 기준으로 그룹화 Map<Integer, List<String>> groupedByLength = words.stream() .collect(Collectors.groupingBy(String::length)); System.out.println(groupedByLength); //{5=[apple, grape], 6=[banana, orange], 4=[kiwi]}
- partitioningBy() 메서드
- 조건에 따라 스트림을 두 개의 그룹으로 분리하는 방식
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 숫자를 짝수와 홀수로 분할 Map<Boolean, List<Integer>> partitioned = numbers.stream() .collect(Collectors.partitioningBy(n -> n % 2 == 0)); System.out.println("짝수: " + partitioned.get(true)); // 짝수: [2, 4, 6, 8, 10] System.out.println("홀수: " + partitioned.get(false)); // 홀수: [1, 3, 5, 7, 9]
- groupinBy()
- 데이터 결합
- joining()
- 스트림의 요소들을 하나의 문자열로 결합
// 모든 사람들의 이름을 쉼표로 쉽게 구분하여 하나의 문자열로 결합 String nameCombined = people.stream() .map(Person::getName) .collect(Collectors.joining(", "));
- reducing()
- 스트림의 요소들을 결합하거나 집계하는 데 사용
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // 숫자 리스트의 합계 계산 Optional<Integer> sum = numbers.stream() .collect(Collectors.reducing((a, b) -> a + b)); System.out.println("합계: " + sum.orElse(0)); // 합계: 15
- collectingAndThen()
- 스트림의 요소들을 수집한 후에 다른 작업을 수행할 때 유용
List<String> words = Arrays.asList("apple", "banana", "orange", "kiwi", "grape"); // 문자열 리스트의 길이의 합계를 계산하고 그 결과에 제곱을 적용 int sumOfLengthsSquared = words.stream() .collect(Collectors.collectingAndThen( Collectors.summingInt(String::length), result -> result * result)); System.out.println("길이의 합계의 제곱: " + sumOfLengthsSquared); // 62
- joining()
- 목록, 세트, 맵 수집
병렬 스트림과 성능
💡
데이터 처리 작업을 여러 스레드에 분산시켜 처리 속도를 향상시키는 방식으로,
Parallel() 메서드를 사용하여 순차 스트림을 병렬 스트림으로 변환 가능
Parallel() 메서드를 사용하여 순차 스트림을 병렬 스트림으로 변환 가능
- 내부적 동작 원리
- 큰 작업은 작은 단위로 분할하고, 이 작은 단위들을 병렬적으로 처리한 후 마지막에 결과를 합치는 방식 ⇒ 분할-정복 알고리즘
- 분할
- Fork/Join 프레임워크를 사용하여 스트림의 데이터를 여러 부분으로 나눈다.
- 병렬 처리
- 나누어진 각 부분은 별도의 스레드에서 처리된다
- ForkJoinPool을 활용하여 작업을 여러 스레드에 분산 시켜 다른 스레드에서 동시에 실행된다.
- 결과 합치기
- 각 스레드에서 처리된 결과는 마지막에 합쳐져 최종 결과를 형성
- 주의해야 할 점
- 스레드 오버헤드
- 병렬 처리는 추가적인 스레드 관리 오버헤드가 발생
- 작업이 충분히 크기 않거나, CPU 연산 집약적이지 않은 경우 순차 처리가 더 효율적
- 순서 보장 X
- 데이터의 순서가 보장되지 않는다.
- 스레드 불 안전성
- 여러 스레드가 동시에 데이터에 접근할 수 있다.
- 메모리 일관성 오류에 대해 주의해야 한다.
- 상태를 공유하는 작업은 피해야 한다.
- 리소스 경쟁
- 시스템의 다른 프로세스나 스레드와 CPU 자원을 경쟁한다
- 전체 시스템 성능에 영향을 줄 수 있다.
- 스레드 오버헤드
- 예시
- 병렬 스트림을 사용하여 짝수만 필터링하고, 그 제곱의 합을 계산
List<Integet> data = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); int sum = data.parallelStream() .filter(n -> n % 2 ==0) .mapToInt(n -> n * n) .sum();
스트림 활용을 위한 팁
스트림 연산의 순서
💡
스트림의 연산 순서는 성능에 큰 영향을 미친다.
- 예시
- 필터링 연산을 매핑 연산 전에 수행하면, 처리해야 할 데이터양을 줄일 수 있어 성능이 향상
// 비효율적인 순서 stream.map(expensiveOperation) .filter(condition) .collect(Collectors.toList()); // 효율적인 순서 stream.filter(condition) .map(expensiveOperation) .collect(Collectors.toList());
최적화된 터미널 연산 사용
💡
특정 작업에 맞는 최적화된 터미널 연산을 선택하는 것이 매우 중요
- 예시
- 단일 요소를 찾기 위해서는 findFirst나 findAny를 사용하는 것이 좋다.
- findFirst()
- 스트림의 첫 번째 요소 반환
- findAny()
- 스트림의 임의의 요소 반환
- 어떤 요소를 반환할지 정확하게 예측 불가능
- findFirst()
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // findFirst Optional<Integer> firstEven = numbers.stream() .filter(n -> n % 2 == 0) .findFirst(); System.out.println(firstEven.orElse(-1)); // 2 // findAny Optional<Integer> anyEven = numbers.stream() .filter(n -> n % 2 == 0) .findAny(); System.out.println(anyEven.orElse(-1)); // 어떤 짝수가 반환될지 예측 불가능
- 단일 요소를 찾기 위해서는 findFirst나 findAny를 사용하는 것이 좋다.
순차 스트림 vs 병렬 스트림
💡
작업의 종류와 데이터의 크기에 따라 순차 스트림이 병렬 스트림보다 더 효율적일 수도 있다. 병렬 스트림은 주로 데이터 처리량이 많고 cpu 집약적인 작업에 더욱 적합하다.
- 예시
List<Integet> largeList = new ArrayList<>(); for(int i = 0; i< 1_000_000; i++){ largeList.add(i); } // 순차 스트림 long startTime = System.currentTimeMillis(); long count = largeList.stream() .filiter(n -> n % 2 == 0) .count(); long endTime = System.currentTimeMillis(); System.out.println((endTime - startTime) + "ms"); // 병렬 스트림 startTime = System.currentTimeMillis(); count = largeList.parallelStream() .filter(n -> n% 2 == 0) .count(); endTime = System.currentTimeMillis(); System.out.println((endTime - startTime) + "ms");
무한 스트림 주의
💡
Stream.generate나 Stream.iterate와 같은 무한 스트림을 사용할 때는 명시적인 종료 조건이 반드시 필요하며, 이를 간과하면 메모리 초과 오류가 발생한다.
- 예시
Stream<Integer> infiniteStream = Stream.iterate(0 , n -> n + 1); infiniteStream.limit(10) .forEach(System.out::println); // 0~9까지 출력
디버깅을 위한 peek 사용
💡
peek 연산은 스트림 내의 요소에 대한 중간 처리 과정을 볼 수 있어 디버깅 작업에 유용
- 성능에 영향을 주므로, 주의해서 사용해야 한다.
- 예시
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integet> doubled = numers.stream() .peek(n -> System.out.println(" 원래 값 : " + n)) .map(n -> n * 2) .peek(n -> System.out.printLn("2배 곱한 값 : " + n)) .collect(Collectors.toList());
스트림 재사용 주의
💡
스트림은 재사용할 수 없다. 즉 한번 사용된 스트림은 다시 사용할 수 없으며, 재사용을 시도하면 IllegalStateException이 발생한다.
- 필요한 경우 새 스트림을 생성해야 한다.
- 예시
Stream<Integet> stream = Stream.of(1, 2, 3); stream.forEach(System.out::println); try{ stream.forEach(System.out::println); //에러 발생 } catch(IllegalStateException e){ System.out.println(e); }
728x90