스트림(Stream) 개념 정리
1. 스트림이란
- JDK 1.8 에서 등장한 개념
- 데이터를 담고 있는 저장소가 아님
- 데이터(컬렉션, 배열 등)를 흐르듯이 처리하는 방법
- 함수형 프로그래밍 스타일로 데이터 변환/필터링/집계할 수 있음.
- 데이터를 하나씩 꺼내서 가공하고, 필요하면 결과만 뽑아내는 흐름을 만드는것
- 코드가 짧고 읽기 쉬워진다
👇 요약
배열이나 리스트를 for문 쓰지 않고 가볍게 가공하는 방식
2. 스트림 특징
특징 | 설명 |
데이터 소스 변경 X | 스트림은 원본 데이터를 변경하지 않음 |
1회용 | 스트림은 한 번만 탐색하고 끝남(재사용 불가) |
지연(lazy)연산 | 중간 연산은 실행 안 하고 모아뒀다가, 최종 연산 할 때 한번에 처리 |
자동 최적화 | 내부적으로 최적화(병렬화, 합성작업) 해서 성능 좋게 처리함 |
병렬 처리 가능 | .paralleStream() 쓰면 멀티 코어로 동시에 처리가능 |
3. 스트림 구성 요소
단계 | 설명 |
스트림 생성 | 데이터를 스트림으로 만들어줌. |
중간 연산 | 스트림을 변형하거나 거르는 과정, 여러개의 파이프 라인을 붙일 수 있다. 스트림을 리턴 할 뿐 실제 연산 결과는 바로 만들지 않는다. 데이터 변형, 필터링, 정렬 등 연결된 파이프라인에 대한 준비만 하며 최종 연산시 실행된다. filter() -> map() -> sorted() 순으로 처리하면 효율적임 limit(), skip()도 결과 도출에 영향을 주지 않는 선에서 가능한 앞에서 처리 연산 순서를 잘 짜면 스트림이 lazy하게 동작하면서 비용을 줄일 수 있다. |
최종 연산 | 스트림을 소비해서 결과를 만듦. 최종 연산이 호출되면 스트림은 더 이상 사용할 수 없다. |
/**
* Stream<T>객체를 선언할 때도
* 중간연산은 몇개가 껴있든 상관없지만
* 최종연산을 사용하면 컴파일 요류가 나는것을 확인할 수 있다.
*/
Stream<String> s = Stream.of("A", "B", "C")
.filter()
.map(String::toUpperCase);
//.forEach(System.out::println);
4. 스트림 처리 흐름
[데이터 소스]
↓
[스트림 생성]
↓
[중간 연산 (필터, 변환, 정렬 등)] (파이프라인 구축)
↓
[최종 연산 (결과 뽑기, 집계 등)] (일괄 실행)
List<String> list = Arrays.asList("a", "bb", "ccc");
list.stream()
.filter(s -> s.length() >= 2)
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
//기대 출력값 : "BB", "CCC"
System.out.println(list);
//기대 출력값 : "a", "bb", "ccc"
5. 메서드 총정리
생성 메서드 | 설명 | 예시 |
Stream.of(T...values) | 여러 값을 스트림으로 만듦 | Stream.of("A", "B", "C") |
Arrays.stream(array) | 배열을 스트림으로 변환 | Arrays.stream(arr) |
Collection.stream() | 자바 Collection객체에서 스트림 생성 | map.stream(), list.stream(), set.stream() |
중간연산 메서드 | 설명 | 예시 |
filter(Predicate) | 조건에 맞는 요소만 통과 | |
map(Function) | 요소변환(ex.String -> length) | Stream<Integer> i = strg.map(Integer::parseInt); |
mapToInt(ToIntFunction) | 요소를 int 타입 변환 | mapToInt(User::getAge), mapToInt(Integer::parseInt) |
mapToLong(ToLongFunction) | 요소를 long 타입 변환 | mapToLong(Long::parseLong) |
mapToDouble(ToDoubleFunc) | 요소를 Double 타입 변환 | mapToDouble(Double::parseDouble) |
flatMap(Function) | 중첩된 스트림을 하나로 평탄화 | flatMap(s -> Arrays.stream(s.split(" "))) |
distinct() | 중복제거 | distinct() |
sorted() | 자연 정렬 | sorted() |
sorted(Comparator) | Comparator기준으로 정렬 | sorted(new Comparator ....) |
peek(Consumer) | 중간에서 요소를 훔쳐봄 (디버깅 용도) | peek(s -> log.info("현재 값 = "+s)) |
limit(long n) | 최대 n개까지만 자름 | limit(5) |
skip(long n) | 앞에서 n개 건너뜀 | skip(10) |
최종연산 메서드 | 설명 | 예시 |
toArray() | 배열로 변환 | String[] arr = stream.toArray() |
forEach(Consumer) | 요소 하나씩 처리(리턴값 없음) | forEach(System.out :: println) |
forEachOrdered(Consumer) | parallelStream 사용시 순서보장 | forEachOrdered(System.out :: println) |
collect(Collectors) | 결과를 모음 (새 객체 리턴) | java.Collection 객체(map, list, set)으로 리턴 |
toList() | List<T> 로 리턴 | collect(Collectors.toList()) |
toSet() | Set<T> 로 리턴 | collect(Collectors.toSet()) |
toMap() | Map<T> 로 리턴 | collect(Collectors.toMap(s->s, s->s.length)) |
joining | 문자열 합치기 | collect(Collectors.joining(", ")) |
groupingBy() | 조건 결과별 그룹핑 분류 | collect(Collectors.groupingBy(String::length)) |
partitioningBy | 조건으로 true/false 그룹 분류 | collect(Collectors.partitioningBy(s -> s.length > 4)) |
reduce(BinaryOperator) | 요소를 하나로 합침 (합계, 문자 합치기) |
reduce(0, (a,b) -> a+b) reduce("", (a,b) -> a + b) |
count() | 개수 세기 | count() |
min(Comparator) | 최솟값 찾기 | min(new Comparator ....) |
max(Comparator) | 최댓값 찾기 | max(new Comparator ....) |
anyMatch(Predicate) | 하나라도 조건 만족하면 true | anyMatch(s -> s.startsWith("A")) |
allMatch(Predicate) | 모두 만족하면 true | allMatch(s -> s.startsWith("A")) |
noneMatch(Predicate) | 아무것도 만족 안하면 true | noneMatch(s -> s.noneMatch("A")) |
findFirst() | 첫번째 요소 찾기 (Optional) | Optional<String> first = list.stream().findFirst(); |
findAny() | 아무거나 하나 찾기 (Optional) | Optional first = list.stream(). findAny(); |
특수 스트림 메서드 | 설명 | 예시 |
** 특수 스트림(IntStream, LongStream, DoubleStream = primitive 타입) ** (중간스트림) primitive stream은 성능이 좋지만 boxed()로 wrapper로 변경시 오토박싱 때문에 성능 저하가 발생 할 수 있다. |
||
range(int start, int end) | 범위스트림 생성 (이상, 미만) | range(1,5) //1,2,3,4 |
rangeClosed(int start, int end) | 범위스트림 생성 (이상, 이하) | range(1,5) //1,2,3,4,5 |
sum() | 합계 | sum() |
average() | 평균 | average() |
min() / max() | 최소 / 최대 | min(), max() |
boxed() | primitive -> wrapper 변환 | List<Integer> list = IntStream .rangeClosed(1, 3) .boxed() .collect(Collectors.toList()); |
6. 병렬 스트림(parallelStream) 사용법 및 주의사항
- 내부적으로 ForkJoinPool.commonPool을 사용 하는 비동기/블로킹 방식
commonPool()은 JVM기동시 core*1배수-1의 싱글톤 workerThreadPool로 정의되며 수정 불가 - parallelStream의 요소 각자 독립적인 쓰레드에서 실행 되는 로직이다.
첫번째 요소는 최종 연산까지 끝났는데 마지막 요소는 ForkJoinPool.commonPool 갯수제한으로
첫관문도 넘지 못했을 수 있음. - 장점
데이터가 많아서 병렬처리로 성능을 향상 하고싶다면 빠른 처리 가능 - 단점
내부 병렬처리용 쓰레드 풀의 갯수를 조절 할 수 없다.
⚠️ 내부 병렬 처리는 무조건 commonPool을 사용하기 때문⚠️
이런 경우 CustomThreadPool + stream() 처리가 낫다
순서가 중요한 연산(forEachOrdered, findFirst)에서는 예측 불가능한 결과
stateful연산(limit, sorted)과 병렬 조합시 성능 저하 발생
7. Optional을 활용한 예외 처리
- Optional<T> 타입으로 리턴하는 최종 스트림은 이점을 활용해서 예외 처리 해주면 좋다
findFirst() | findAny() | max(Comparator) | min(Comparator) |
reduce(BinaryOperator) | collect(Collectors.maxBy()) | collect(Collectors.minBy()) |
8. 스트림과 컬렉션의 차이점 정리
비교 항목 | Stream | Collection |
저장 vs 계산 | 계산 중심 | 데이터 저장 |
재사용 | 1회용 | 가능 |
지연 연산 | 있음 | 없음 |
멀티스레드 | 병렬처리 가능(parallelStream) | 직접 처리 필요 |
'Programing' 카테고리의 다른 글
[Java] 함수형 인터페이스와 람다식 (0) | 2025.05.09 |
---|---|
[Java] Optional Class (0) | 2025.05.08 |
[Java] IO vs NIO (1) | 2025.04.30 |
메모리 덤프(dump) 분석 [ jps, jmap, jhat ] (0) | 2022.05.16 |
Scanner 클래스 사용법 (0) | 2021.09.14 |