스트림 생성
스트림은 자바 8부터 추가된 기능으로 데이터 처리에 있어 간결하고 효율적인 코드 작성을 가능하게 해줌
특히 중간 연산과 최종 연산을 구분하며, 지연 연산을 통해 불필요한 연산을 최소화함
CreateStreamMain.java : 스트림을 생성하는 대표적인 방법들을 코드로 알아보자
package stream.operation;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class CreateStreamMain {
public static void main(String[] args) {
System.out.println("1. 컬렉션으로부터 생성");
List<String> list = List.of("a", "b", "c");
Stream<String> stream1 = list.stream();
stream1.forEach(System.out::println);
System.out.println("2. 배열로부터 생성");
String[] arr = {"a", "b", "c"};
Stream<String> stream2 = Arrays.stream(arr);
stream2.forEach(System.out::println);
System.out.println("3. Stream.of() 사용");
Stream<String> stream3 = Stream.of("a", "b", "c");
stream3.forEach(System.out::println);
System.out.println("4. 무한 스트림 생성 - iterate()");
// iterate : 초기값과 다음값을 만드는 함수를 지정
Stream<Integer> infiniteStream = Stream.iterate(0, n-> n+2); // 0, 2, 4, 6...
infiniteStream.forEach(System.out::println); // 무한 출력
infiniteStream.limit(3).forEach(System.out::println); // 0, 2, 4 만 출력
System.out.println("5. 무한 스트림 생성 - generate()");
// generate : Supplier를 사용해 무한 생성
Stream<Double> randomStream = Stream.generate(Math::random);
randomStream.limit(3).forEach(System.out::println);
}
}
-> 컬렉션, 배열, Stream.of는 기본적으로 유한한 데이터 소스로부터 스트림 생성
-> iterate, generate는 별도 종료 조건이 없다면 무한히 데이터를 만들어내는 스트림 생성으로 필요한 만큼(limit)만 사용해야 함
중간 연산
스트림 파이프라인에서 데이터를 변환, 정렬 등을 하는 단계

FlatMap
: 중간 연산 중 하나
: map은 각 요소를 하나의 값으로 변환하지만 FlatMap은 각 요소를 스트림으로 변환한 뒤 그 결과를 하나의 스트림으로 평탄화해줌
MapVsFlatMapMain.java
package stream.operation;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class MapVsFlatMapMain {
public static void main(String[] args) {
List<List<Integer>> outerList = List.of(
List.of(1,2),
List.of(3,4),
List.of(5,6)
);
System.out.println("outerList = " + outerList);
ArrayList<Integer> forResult = new ArrayList<>();
for(List<Integer> list : outerList) {
for(Integer i : list) {
forResult.add(i);
}
}
System.out.println("forResult = " + forResult);
// 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);
}
}
실행 결과

1) map
> 이중 구조가 그대로 유지됨
> 각 요소가 Stream 형태가 되므로 결과가 List<Stream<Integer>>가 됨
> java.util.stream.ReferencePipiline$.. 형태로 보임
2) flatMap
> flatMap 호출 시 list -> list.stream()이 호출되며 내부의 3개 List<Integer>를 Stream<Integer>로 변환
> flatMap은 Stream<Integer> 내부 값을 꺼내 외부 Stream에 포함
> 따라서 Stream<Integer>가 됨
=> flatMap은 중첩 구조를 일차원으로 펼치는 데 사용된다.
Optional 간단 설명
최종 연산을 시작하기 전 자바가 제공하는 Optional 클래스를 간단히 알아보자
Optional 클래스
> 내부에 하나의 값(value)을 가짐
> isPresent()를 통해 그 값이 있는지 없는지 확인할 수 있다.
> get()으로 내부의 값을 꺼낼 수 있다. 없다면 예외가 발생한다.
> Optional은 이름 그대로 필수가 아니라 옵션이라는 뜻 : 내부에 값이 있을 수도 있고 없을 수도 있다는 뜻
OptionalSimpleMain.java
package stream.operation;
import java.util.Optional;
public class OptionalSimpleMain {
public static void main(String[] args) {
Optional<Integer> optional1 = Optional.of(10);
System.out.println("optional1 = " + optional1);
// 값이 있는지 확인할 수 있는 안전한 메서드 제공
if(optional1.isPresent()) {
Integer i = optional1.get();
System.out.println("i = " + i);
}
Optional<Object> optional2 = Optional.ofNullable(null);
System.out.println("optional2 = " + optional2);
if(optional2.isPresent()) {
Object o = optional2.get();
System.out.println("o = " + o);
}
// 값이 없는 Optional에서 get 호출 시 NoSuchElementException 발생
Object o2 = optional2.get();
}
}
실행 결과

-> Optional은 내부에 값을 담아두고 그 값의 null 여부를 체크하는 isPresent()와 같은 안전한 체크 메서드 제공
-> null 값으로 인한 오류를 방지하고 코드에서 "값이 없을 수도 있다"는 상황을 명시적으로 표현하기 위해 사용
최종 연산
스트림 파이프라인의 끝에 호출되어 실제 연산을 수행하고 결과를 만들어낸다.
최종연산이 실행된 후 스트림은 소모되어 더 이상 사용할 수 없다.

* reduce를 사용할 때 초깃값을 지정하면, 스트림이 비어 있어도 초깃값이 결과가 된다. 초깃값이 없으면 Optional을 반환한다.
* findFirst(), findAny()도 결과가 없을 수 있으므로 Optional을 통해 값 유무를 확인해야 한다.
기본형 특화 스트림
자바에서는 IntStream, LongStream, DoubleStream의 세가지 형태를 제공해 기본 자료형에 특화된 기능을 사용할 수 있다.

* 기본형 특화 스트림의 숫자 범위 생성 기능
1) range(int startInclusive, int endExclusive) : 시작값 이상 ~ 끝값 미만
-> IntStream.range(1, 5) -> [1, 2, 3, 4]
2) rangeClosed(int startInclusive, int endInclusive) : 시작값 이상 ~ 끝값 포함
-> IntStream.rangeClosed(1, 5) -> [1, 2, 3, 4, 5]
주요 기능 및 메서드
: 합계, 평균 등 자주 사용하는 연산을 편리한 메서드로 제공
: 타입 변환과 박싱/언박싱 메서드도 제공

성능 - 전통적 for문 vs 스트림 vs 기본형 특화 스트림
* 실제로는 데이터 양, 연산 종류, JVM 최적화 등에 따라 달라짐
> 전통적인 for문이 보통 가장 빠름
> 스트림보다 전통적 for문이 1.5 ~ 2배 정도 빠름
> 기본형 특화 스트림(IntStream 등)은 전통적 for문에 가까운 성능을 보여줌
실무에선?
극단적 성능이 필요한 경우가 아니라면, 코드의 가독성과 유지보수성을 위해 스트림 API를 사용하는 것이 보통 나음
'인프런 > 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍' 카테고리의 다른 글
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 10. Optional (1) | 2025.06.25 |
|---|---|
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 9. 스트림 API 3 - 컬렉터 (0) | 2025.06.23 |
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 8. 스트림 API 1 - 기본 (3) | 2025.06.14 |
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 7. 메서드 참조 (0) | 2025.05.28 |
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 6. 람다 vs 익명 클래스 (0) | 2025.05.28 |