자바의 Stream API는 Java 8부터 도입된 기능으로, 컬렉션(Collection) 데이터를 처리할 때 선언형(Declarative) 방식으로 간결하고 효율적으로 작업할 수 있도록 도와주는 기능이다.
쉽게 말하면, 데이터를 필터링하고, 변형하고, 집계하는 일련의 작업을 파이프라인처럼 연결해서 처리할 수 있는 도구라고 보면 된다.
Stream API는 단독으로 뭔가 하는 게 아니라
"데이터 → Stream → 연산들"이라는 전체 흐름 속에서 사용된다는 점이 핵심이다.
1. Stream 만드는 방법 (데이터 → Stream)
Stream API를 쓰기 위해서는 반드시 "스트림 객체"를 만들어야 한다.
즉, Stream은 “데이터 소스”로부터 시작해서, 그 위에 중간 연산과 최종 연산을 쌓는 구조이다.
가장 많이 쓰는 순서부터 한 번 작성해 보겠다.
1. 컬렉션(Collection)으로부터
List<String> list = List.of("A", "B", "C");
Stream<String> stream = list.stream();
- .stream() → 순차 스트림
- .parallelStream() → 병렬 스트림 (멀티코어 처리)
list.stream()은 실제로 Stream<String> stream = list.stream(); 이렇게 변수에 담는 걸 생략한 것 뿐이다.
2. 배열로부터
String[] arr = {"A", "B", "C"};
Stream<String> stream = Arrays.stream(arr);
- Collections를 상속 받지 않은 Arrays(배열)은 Arrays.stream()을 이용하여 stream객체를 생성 가능하다.
3. Stream 클래스의 정적 메서드 사용
Stream<String> stream = Stream.of("A", "B", "C");
- .of() → 가변 인자를 스트림으로
- .generate(Supplier) → 무한 스트림 (ex: 랜덤값 반복 생성)
- .iterate(seed, UnaryOperator) → 무한 반복 스트림
예:
Stream<Integer> nums = Stream.iterate(1, n -> n + 1); // 1, 2, 3, ...
Stream<String> stream = Stream.of("a", "b", "c");
Stream<int[]> intArrayStream = Stream.of(new int[]{1, 2, 3});
4. 파일 등 외부 자원
Stream<String> lines = Files.lines(Paths.get("file.txt"));
5. Stream.builder() 을 활용하여 수동으로 만들 때
Stream.Builder<String> builder = Stream.builder();
builder.add("apple");
builder.add("banana");
builder.add("cherry");
Stream<String> stream = builder.build();
간단한 버전 :
Stream<String> stream = Stream.<String>builder()
.add("apple")
.add("banana")
.add("cherry")
.build();
주의할 점 :
- .build()를 호출한 후에는 .add()를 더 이상 사용할 수 없음 (불변 스트림 생성)
- 너무 많은 요소를 직접 add()로 넣는 건 귀찮음 → 이럴 땐 List.stream()이나 Stream.of()가 더 나음
Stream 만들기 요약 정리
방법 | 설명 |
list.stream() | 컬렉션에서 스트림 생성 |
Arrays.stream() | 배열에서 스트림 생성 |
Stream.of(...) | 가변 인자 스트림 생성 |
Stream.builder() | 수동으로 add() 해서 스트림 생성 |
Stream.generate() | Supplier 기반 무한 스트림 |
Stream.iterate() | 초기값과 함수 기반 반복 스트림 |
Stream 객체를 만들었다면 이걸 활용해야 한다.
만들어지 Stream 객체를 사용하기 위해서는 2단계 과정을 더 거쳐야한다. 바로 중간 연산과 최종 연산이다.
1. 중간 연산 (Intermediate Operations)
스트림을 변형시키고 연결만 하고, 실제 실행은 최종 연산이 호출될 때까지 지연됨 (lazy evaluation).
연산 | 설명 | 예시 |
filter(Predicate) | 조건에 맞는 요소만 통과 | filter(x -> x > 10) |
map(Function) | 요소를 다른 형태로 변환 | map(String::length) |
flatMap(Function) | 중첩된 스트림을 평탄화 | flatMap(list -> list.stream()) |
sorted() | 기본 정렬 | sorted() |
sorted(Comparator) | 사용자 정의 정렬 | sorted(Comparator.reverseOrder()) |
distinct() | 중복 제거 | distinct() |
limit(n) | 앞에서 n개만 선택 | limit(5) |
skip(n) | 앞에서 n개 건너뜀 | skip(3) |
peek(Consumer) | 디버깅용: 요소를 들여다봄 | peek(System.out::println) |
2. 최종 연산 (Terminal Operations)
스트림 연산을 실행하고 결과를 반환함.
여기서 연산이 실제로 수행됨.
연산 | 설명 | 반환형 |
forEach(Consumer) | 각 요소에 대해 작업 | void |
collect(Collector) | 요소들을 수집 (리스트, 집합 등으로) | List, Set 등 |
reduce() | 누적해서 하나의 값으로 합침 | Optional<T> |
count() | 요소 수 계산 | long |
anyMatch(), allMatch(), noneMatch() | 조건에 대한 논리값 | boolean |
findFirst(), findAny() | 요소 하나 반환 | Optional<T> |
toArray() | 배열로 변환 | Object[] 또는 T[] |
중간 연산의 경우 lazy하여 최종연산이 실행되기 전까지 아무 일도 일어나지 않는다.
최종 연산은 한 번만 실행 가능하며, 중간 연산은 여러번 사용해도 된다.
3. 쉽게 기억하는 비유!
- Stream = 컨베이어 벨트
- 중간 연산 = 벨트 위에서 필터링, 변형, 정렬하는 장치들
- 최종 연산 = 완성품을 포장하거나 소비하는 마지막 과정
- 포장하고 나면, 벨트는 멈추고 재사용 불가
예시 :
List<String> names = List.of("Alice", "Bob", "Charlie", "David");
long count = names.stream()
.filter(name -> name.length() > 3) // 중간 연산
.map(String::toUpperCase) // 중간 연산
.distinct() // 중간 연산
.count(); // 최종 연산
System.out.println(count); // 3
Stream API를 활용하려면 람다(Lambda)식도 알아야한다.
참 귀찮은 일이지만 다음 포스트에 한 번 람다식에 관해 써보겠다.
'공부 > 자바(Java)' 카테고리의 다른 글
Java) Java의 객체지향 프로그래밍(OOP)이란? (1) | 2025.04.21 |
---|---|
Java) 람다식(Lambda expression)과 함수형 인터페이스란? (0) | 2025.04.06 |
Java) Set이란? (세트 간단 설명, 사용법, 예제) (0) | 2025.04.06 |
Java) Arrays VS Collections | ( Arrays와 Collections의 간단 설명, 사용법, 예제) (0) | 2025.04.05 |
Java) Queue란? (큐 간단 설명, 사용법, 예제) (3) | 2025.04.05 |