스트림(stream)이란 ?
스트림이란 이름에서도 알 수 있듯, 데이터를 효율적으로 처리할 수 있는 흐름을 말한다.
스트림은 선언형 스타일로 가독성이 굉장히 뛰어나며 데이터 준비 → 중간 연산 → 최종 연산
순으로 처리된다. 스트림은 컬렉션(List, Set 등) 과 함께 자주 이용되기도 한다.
스트림의 특징
- 데이터 흐름(데이터 스트림)
- 스트림은 데이터를 한 방향으로 전달하며, 여러 연산을 적용할 수 있음.
- 컬렉션과 다르게 데이터를 직접 저장하지 않음
- 리스트(
List
), 셋(Set
), 맵(Map
)과 달리 데이터를 저장하는 구조가 아님. - 즉, 스트림은 데이터를 "흐르게" 하면서 변형하는 도구.
- 리스트(
- 한 번 사용하면 다시 사용할 수 없음
- 스트림은 한 번 실행되면 재사용할 수 없음.
- 중간 연산(Intermediate Operations)과 최종 연산(Terminal Operations)
- 중간 연산: 데이터를 변환 (
filter
,map
,sorted
등) - 최종 연산: 결과를 반환 (
collect
,forEach
,count
등) - 중간 연산만 사용하면 아무 동작도 수행하지 않음 → 최종 연산을 호출해야 실행됨.
- 중간 연산: 데이터를 변환 (
스트림의 주요 연산
스트림은 데이터를 처리하기 위해 여러 API를 제공한다. 관련 내용을 더 자세히 알고 싶으면 https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html
다음 공식문서를 참고하면 된다.
단계 | 설명 | 주요 API |
---|---|---|
1. 데이터 준비 | 컬렉션을 스트림으로 변환 | stream() , parallelStream() |
2. 중간 연산 등록 (즉시 실행되지 않음) |
데이터 변환 및 필터링 | map(), filter(), sorted() |
3. 최종 연산 | 최종 처리 및 데이터 변환 | collect(), forEach(), count() |
1. 스트림 생성
리스트나 셋 등의 컬렉션에 stream()
메서드를 호출해 생성합니다
// 컬렉션으로부터 생성하기
List<String> names = Arrays.asList("민수", "영희", "철수");
Stream<String> stream = names.stream();
// 배열로부터 생성하기
int[] numbers = {1, 2, 3};
IntStream stream = Arrays.stream(numbers);
2. 중간 연산
중간 연산은 데이터를 중간에 가공하거나 필터링하는 역할을 한다. 또한 중간 연산은 여러 번 이어 붙여 사용할 수 있으며, 메서드 체이닝을 통해 처리할 수 있다.
즉시 실행되지 않고 최종 연산이 실행될 때 한 번에 처리된다. (지연 평가).
중간 연산 | 역할 | 예시 |
---|---|---|
map() |
데이터를 다른 형태로 변환 | .map(x -> x * x) |
filter() |
조건에 맞는 데이터만 추출 | .filter(n -> n > 5) |
distinct() |
중복 데이터 제거 | .distinct() |
sorted() |
정렬 | .sorted() 또는 .sorted(Comparator) |
중간 연산 사용 예시
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.filter(n -> n % 2 == 1) // 홀수만 필터링 (중간 연산)
.map(n -> n * n) // 제곱으로 변환
.sorted() // 정렬
.forEach(System.out::println); // 최종 연산으로 출력
3. 최종 연산
스트림의 최종 연산은 결과를 도출해내는 것이고, 최종 연산은 Stream의 처리가 끝난 후에만 수행된다. 최종 연산 후에는 스트림이 닫히게 되고 더 이상 사용할 수 없다.
최종 연산 | 역할 | 예시 |
---|---|---|
forEach() |
각 데이터를 출력하거나 특정 작업 수행 | .forEach(System.out::println) |
collect() |
스트림을 다시 컬렉션으로 반환 | .collect(Collectors.toList()) |
reduce() |
스트림의 요소를 하나의 값으로 축소 | .reduce(0, Integer::sum) |
count() |
스트림의 요소 수를 반환 | .count() |
anyMatch() , allMatch() , noneMatch() |
조건 매칭 여부 판단 | .anyMatch(n -> n > 10) |
최종 연산 사용 예시
// reduce 사용 예시
int sum = numbers.stream()
.reduce(0, Integer::sum);
// collect 사용 예시
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
// forEach 사용 예시
list.forEach(요소 -> 실행할 코드);
list.forEach(list -> System.out.println(list))
.forEach(System.out::println); // 메서드 참조도 가능함.
스트림과 함께 쓰이는 알아두면 좋은 문법 !
::
(더블 콜론 연산자) - 메서드 참조(Method Reference)
::
(더블 콜론 연산자)는 메서드 참조(Method Reference) 를 나타내며, 람다 표현식을 더 간결하게 작성할 수 있도록 도와주는 기능이다.
(x, y) -> Math.max(x, y)
이 자체도 매우 간략해 보이지만, (x, y)
부분이 중복된다. 또한 ->
를 통해 또 다른 메서드를 호출한다. 따라서 이를 더 간략하게 만드는 것이다.
간략하게 줄인 것은 다음과 같다.
Math::max; // (x, y) -> Math.max(x, y)
:: 연산자의 종류는 다음과 같다.
종류 | 설명 | 예시 | 람다 표현식 |
---|---|---|---|
1. 정적 메서드 참조 | 클래스::정적메서드 |
Math::abs |
x -> Math.abs(x) |
2. 특정 객체의 인스턴스 메서드 참조 | 객체::인스턴스메서드 |
myObject::method |
x -> myObject.method(x) |
3. 특정 타입의 인스턴스 메서드 참조 | 클래스::인스턴스메서드 |
String::toUpperCase |
s -> s.toUpperCase() |
4. 생성자 참조 | 클래스::new |
ArrayList::new |
() -> new ArrayList<>() |
🔍 스트림 사용의 장점 (왜 스트림을 사용할까?)
- 간결성
- 기존 반복문 및 조건문으로 작성할 때보다 간결하고 직관적인 코드로 유지할 수 있다. 컬렉션 및 데이터를 좀 더 선언적으로 다룰 수 있으며, 성능 최적화 측면에서도 이점이 있다.
- 지연 평가(Lazy evaluation)
- 최종 연산이 호출되기 전까지 중간 연산이 실행되지 않아, 불필요한 연산을 줄일 수 있다.
- 병렬 처리 지원
- 스트림은 병렬 스트림(
parallelStream()
)을 제공하여 멀티코어 환경에서 성능을 향상시킬 수 있다. - 가독성 향상
- 메서드 체이닝과 람다 표현식을 통해 깔끔하고 가독성이 뛰어난 코드 작성이 가능하다. 특히 컬렉션의 데이터를 필터링하거나, 변형하거나, 최종적으로 데이터를 집계할 때 유용하게 사용할 수 있다.
'Language > Java' 카테고리의 다른 글
[Java] if-else와 try-catch에 대해서 (0) | 2025.03.14 |
---|---|
[Java] 제네릭(Generic) (0) | 2025.03.06 |
[Java] 기본형과 참조형 (0) | 2025.03.06 |
[Java] 익명 클래스 (Annoymous Class)와 람다 (Lamda) (0) | 2025.03.05 |
[Java] 컬렉션 (Collection) (0) | 2025.03.04 |