스트림 API2 - 기능
#Java/adv3
스트림 생성
- 스트림이 제공하는 다양한 스트림 생성, 중간 연산, 최종 연산을 자세히 알아보자.
생성 방법 | 코드 예시 | 특징 |
---|---|---|
컬렉션 | list.stream() |
List, Set 등 컬렉션에서 스트림 생성 |
배열 | Arrays.stream(arr) |
배열에서 스트림 생성 |
Stream.of(...) | Stream.of("a", "b", "c") |
직접 요소를 입력해 스트림 생성 |
무한 스트림(iterate) | Stream.iterate(0, n -> n + 2) |
무한 스트림 생성(초깃값 + 함수) |
무한 스트림(generate) | Stream.generate(Math::random) |
무한 스트림 생성 (Supplier 사용) |
- 컬렉션, 배열, Stream.of 은 기본적으로 유한한 데이터 소스로부터 스트림을 생성한다.
- iterate, generate는 별도의 종료 조건이 없으면 무한히 데이터를 만들어내는 스트림을 생성한다.
- 따라서 필요한 만큼만(limit) 사용해야 한다. 그렇지 않으면 무한 루프처럼 계속 스트림을 뽑아내므로 주의해야 한다.
infiniteStream.limit(3).forEach(System.out::println);
- 스트림은 일반적으로 한 번 사용하면 재사용할 수 없다(소진되면 끝). 따라서,
stream()
으로 얻은 스트림을 여러 번 순회하려면, 다시 스트림을 생성해야 한다.
중간 연산
스트림 파이프라인에서 데이터를 변환, 필터링, 정렬 등을 하는 단계이다.
- 여러 중간 연산을 연결하여 원하는 형태로 데이터를 가공할 수 있다.
- 결과가 즉시 생성되지 않고, 최종 연산이 호출될 때 한꺼번에 처리된다는 특징이 있다(지연 연산).
연산 | 설명 | 예시 |
---|---|---|
filter | 조건에 맞는 요소만 남김 | stream.filter(n -> n > 5) |
map | 요소를 다른 형태로 변환 | stream.map(n -> n * 2) |
flatMap | 중첩 구조 스트림을 일차원으로 평탄화 | stream.flatMap(list -> list.stream()) |
distinct | 중복 요소 제거 | stream.distinct() |
sorted | 요소 정렬 | stream.sorted() / stream.sorted(Comparator.reverseOrder()) |
peek | 중간 처리 (로그, 디버깅, (중간에 뭔가 찍기)) peek는 데이터를 변경하지 않는다. |
stream.peek(System.out::println) |
limit | 앞에서 N개의 요소만 추출 | stream.limit(5) |
skip | 앞에서 N개의 요소를 건너뛰고 이후 요소만 추출 | stream.skip(5) |
takeWhile | 조건을 만족하는 동안 요소 추출 (Java 9+) 정렬된 데이터에 효과적. |
stream.takeWhile(n -> n < 5) |
dropWhile | 조건을 만족하는 동안 요소를 버리고 이후 요소 추출 (Java 9+) | stream.dropWhile(n -> n < 5) |
FlatMap
스트림 중간 연산 중 하나
map
은 각 요소를 하나의 값으로 변환하지만,flatMap
은 각 요소를 스트림(또는 여러 요소)으로 변환한 뒤, 그 결과를 하나의 스트림으로 평탄화(flatten)해준다.
리스트 안의 리스트 중첩 구조인 경우
[
[1, 2],
[3, 4],
[5, 6]
]
flatMap
을 사용하면 이 데이터를 다음과 같이 쉽게 평탄화(flatten)할 수 있다.[1, 2, 3, 4, 5, 6]
// map
List<Stream<Integer>> mapResult = outerList.stream()
.map(list -> list.stream())
.toList();
System.out.println("mapResult = " + mapResult);
// flatMap
List<Integer> flatMapResult = outerList.stream()
.flatMap(list -> list.stream())
.toList();
System.out.println("flatMapResult = " + flatMapResult);
map
의 경우 먼저List<List<Integer>>
를Stream<List<Integer>>
로 만든 뒤, 스트림의 각 원소인List<Integer>
를 stream()으로 변환한 다음, 다시 스트림을 List로 변환해 최종적으로List<Stream<Integer>>
로 변환했다.map
을 쓰면 이중 구조가 그대로 유지된다.
flatMap
을 쓰면 내부의Stream
들을 하나로 합쳐List<Integer>
를 얻을 수 있다.
flatMap(list -> list.stream())
Stream<List<Integer>>
Stream<Integer>
flatMap()
을 호출하면list -> list.stream()
이 호출되면서 내부에 있는 3개의List<Integer>
를Stream<Integer>
로 변환한다.flatMap()
은Stream<Integer>
내부의 값을 꺼내서 외부Stream
에 포함한다. 여기서는 1, 2, 3, 4, 5, 6의 값을 꺼낸다.- 이렇게 꺼낸 1, 2, 3, 4, 5, 6 값 각각이 외부
Stream
에 포함된다. 따라서Stream<Integer>
가 된다.
정리
flatMap
은 중첩 구조(컬렉션 안의 컬렉션, 배열 안의 배열 등)를 일차원으로 펼치는 데 사용된다.- 예를 들어, 문자열 리스트들이 들어있는 리스트를 평탄화하면, 하나의 연속된 문자열 리스트로 만들 수 있다.
Optional 간단 설명
자바 Optional 클래스
package java.util;
public final class Optional<T> {
private final T value;
// ...
public boolean isPresent() {
return value != null;
}
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
// ...
}
Optional
은 내부에 하나의 값(value
)을 가진다.isPresent()
를 통해 그 값(value
)이 있는지 없는지 확인할 수 있다.get()
을 통해 내부의 값을 꺼낼 수 있다. 만약 값이 없다면 예외가 발생한다.Optional
은 이름 그대로 필수가 아니라 옵션이라는 뜻이다.- 이 말은 옵셔널 내부에 값(
value
)이 있을 수도 있고 없을 수도 있다는 뜻이다.
- 이 말은 옵셔널 내부에 값(
Optional<Integer> optional1 = Optional.of(10);
if (optional1.isPresent()) { // 값이 있는지 확인할 수 있는 안전한 메서드 제공
Integer i = optional1.get(); // Optional 안에 있는 값을 획득
System.out.println("i = " + i);
}
Optional<Object> optional2 = Optional.ofNullable(null);
if (optional2.isPresent()) {
Object o = optional2.get();
System.out.println("o = " + o);
}
// 값이 없는 Optional에서 get()을 호출하면 NoSuchElementException이 발생한다.
Object o2 = optional2.get();
Optional
은 내부에 값을 담아두고, 그 값이null
인지 아닌지를 체크할 수 있는isPresent()
와 같은 안전한 체크 메서드를 제공한다. 따라서 안전한 체크 메서드를 통해 체크하고 난 다음에 값이 있을 때만get()
으로 값을 꺼내는 방식으로 사용할 수 있다.Optional
은null
값으로 인한 오류(NullPointerException
)를 방지하고, 코드에서 "값이 없을 수도 있다"는 상황을 명시적으로 표현하기 위해 사용된다. 간단히 말해,null
을 직접 다루는 대신Optional
을 사용하면 값의 유무를 안전하게 처리할 수 있어 코드가 더 명확하고 안정적으로 작성할 수 있다.
최종 연산
- 스트림 최종 연산은 스트림 파이프라인의 끝에 호출되어 실제 연산을 수행하고 결과를 만들어낸다. 최종 연산이 실행된 후에 스트림은 소모되어 더 이상 사용할 수 없다.
연산 | 설명 | 예시 |
---|---|---|
collect |
Collector 를 사용하여 결과 수집 (다양한 형태로 변환 가능) |
stream.collect(Collectors.toList()) |
toList (Java16+) |
스트림을 불변 리스트로 수집 | stream.toList() |
toArray |
스트림을 배열로 변환 | stream.toArray(Integer[]::new) |
forEach |
각 요소에 대해 동작 수행 (반환값 없음) | stream.forEach(System.out::println) |
count |
요소 개수 반환 | long count = stream.count(); |
reduce |
누적 함수를 사용해 모든 요소를 단일 결과로 합침. (초기값을 지정해줄 수 있음. 초깃값이 없으면 Optional 로 반환) |
int sum = stream.reduce(0, Integer::sum); |
min / max |
최솟값, 최댓값을 Optional 로 반환 |
stream.min(Integer::compareTo) , stream.max(Integer::compareTo) |
findFirst |
조건에 맞는 첫 번째 요소 (Optional 반환) |
stream.findFirst() |
findAny |
조건에 맞는 아무 요소나 (Optional 반환)(병렬 스트림에서 효율적으로 동작할 수 있다) |
stream.findAny() |
anyMatch |
하나라도 조건을 만족하는지 (boolean) | stream.anyMatch(n -> n > 5) |
allMatch |
모두 조건을 만족하는지 (boolean) | stream.allMatch(n -> n > 0) |
noneMatch |
하나도 조건을 만족하지 않는지 (boolean) | stream.noneMatch(n -> n < 0) |
정리
- 최종 연산이 호출되면, 그 동안 정의된 모든 중간 연산이 한 번에 적용되어 결과를 만든다.
- 최종 연산을 한 번 수행하면, 스트림은 재사용할 수 없다.
reduce
를 사용할 때 초깃값을 지정하면, 스트림이 비어 있어도 초깃값이 결과가 된다. 초깃값이 없으면Optional
을 반환한다.- 초깃값이 없는데, 스트림이 비어 있을 경우 빈
Optional
(Optional.empty()
)을 반환한다.
- 초깃값이 없는데, 스트림이 비어 있을 경우 빈
findFirst()
,findAny()
도 결과가 없을 수 있으므로 Optional을 통해 값 유무를 확인해야 한다.
기본형 특화 스트림:
스트림 API에는 기본형(primitive) 특화 스트림이 존재한다.
- 자바에서는
IntStream
,LongStream
,DoubleStream
세 가지 형태를 제공하여 기본 자료형(int, long, double)에 특화된 기능을 사용할 수 있게 한다.- 예를 들어,
IntStream
은 합계 계산, 평균, 최솟값, 최댓값 등 정수와 관련된 연산을 좀 더 편리하게 제공하고, 오토박싱/언박싱 비용을 줄여 성능도 향상시킬 수 있다.
- 예를 들어,
스트림 타입 | 대상 원시 타입 | 생성 예시 |
---|---|---|
IntStream |
int |
IntStream.of(1, 2, 3) , IntStream.range(1, 10) , mapToInt(...) |
LongStream |
long |
LongStream.of(10L, 20L) , LongStream.range(1, 10) , mapToLong(...) |
DoubleStream |
double |
DoubleStream.of(3.14, 2.78) , DoubleStream.generate(Math::random) , mapToDouble(...) |
기본형 특화 스트림의 숫자 범위 생성 기능
range(int startInclusive, int endExclusive)
: 시작값 이상, 끝값 미만IntStream.range(1, 5)
→[1, 2, 3, 4]
rangeClosed(int startInclusive, int endInclusive)
: 시작값 이상, 끝값 포함IntStream.rangeClosed(1, 5)
→[1, 2, 3, 4, 5]
기본형 특화 스트림: 주요 기능 및 메서드
- 합계, 평균 등 자주 사용하는 연산을 편리한 메서드로 제공한다. 또한, 타입 변환과 박싱/언박싱 메서드도 제공하여 다른 스트림과 연계해 작업하기 수월하다.
메서드 / 기능 | 설명 | 예시 |
---|---|---|
sum() |
모든 요소의 합계를 구한다. | int total = IntStream.of(1, 2, 3).sum(); |
average() |
모든 요소의 평균을 구한다. OptionalDouble 을 반환. |
double avg = IntStream.range(1, 5).average().getAsDouble(); |
summaryStatistics() |
최솟값, 최댓값, 합계, 개수, 평균 등이 담긴 IntSummaryStatistics (또는 Long /Double ) 객체 반환 |
IntSummaryStatistics stats = IntStream.range(1,5).summaryStatistics(); |
asLongStream(), asDoubleStream() | 타입 변환 | LongStream longStream = IntStream.range(1, 5).asLongStream(); |
mapToInt() ,mapToLong() , mapToDouble() |
매핑+ 타입 변환 : IntStream → LongStream , DoubleStream ...Stream<Integer> IntStream |
LongStream ls = IntStream.of(1,2).mapToLong(i -> i * 10L); IntStream instream = Stream.of(1, 2, 3, 4, 5).mapToInt(i  i) |
mapToObj() |
객체 스트림으로 변환 : 기본형 → 참조형 | Stream<String> s = IntStream.range(1,5).mapToObj(i -> "No: "+i); |
boxed() |
기본형 특화 스트림을 박싱(Wrapper)된 객체 스트림으로 변환 | Stream<Integer> si = IntStream.range(1,5).boxed(); |
sum() , min() , max() , count() |
합계, 최솟값, 최댓값, 개수를 반환 (타입별로 int /long /double 반환) |
long cnt = LongStream.of(1,2,3).count(); |
- 기본형 특화 스트림(
IntStream
,LongStream
,DoubleStream
) 을 이용하면 숫자 계산(합계, 평균, 최대·최소 등)을 간편하게 처리하고, 박싱/언박싱 오버헤드를 줄여 성능상의 이점도 얻을 수 있다. - range(), rangeClosed() 같은 메서드를 사용하면 범위를 쉽게 다룰 수 있어 반복문 대신에 자주 쓰인다.
mapToXxx
,boxed()
등의 메서드를 잘 활용하면 객체 스트림과 기본형 특화 스트림을 자유롭게 오가며 다양한 작업을 할 수 있다.summaryStatistics()
를 이용하면 합계, 평균, 최솟값, 최댓값 등 통계 정보를 한 번에 구할 수 있어 편리하다.- 기본형 특화 스트림을 잘 이용하면 가독성, 성능 모두 잡을 수 있다. 따라서 숫자 중심의 연산에서는 적극 활용하는 것을 고려하자. (기본형 특화 스트림은 사용할 일이 가끔 있다~)
성능 - 전통적인 for문 vs 스트림 vs 기본형 특화 스트림
- 전통적인 for문이 제일 빠르다.
- 전통적인 for문이 스트림에 비해선 1.5~2배 더 빠르다.
- 박싱/언박싱 오버헤드가 존재한다.
- 기본형 특화 스트림은 전통적인 for문에 가까운 성능을 보인다. (거의 비슷하거나 for문이 10%~30% 빠름)
- 박싱/언방식 오버헤드를 피할 수 있고, 내부적으로 최적화된 연산 수행이 가능하다.
- 최적의 선택은 구현 가독성, 유지보수성, 극단적인 성능 필요 유무 등을 고려해야 한다.
'Java > Modern Java(8~)' 카테고리의 다른 글
[Java] 66. Optional (1) | 2025.07.04 |
---|---|
[Java] 65. 스트림 API3 - 컬렉터 (0) | 2025.07.04 |
[Java] 63. 스트림 API1 - 기본 (0) | 2025.07.04 |
[Java] 62. 메서드 참조 (0) | 2025.07.04 |
[Java] 61. 람다 vs 익명 클래스 (0) | 2025.07.04 |