스트림(Stream)을 공부하다 보면 종단 연산과 중간 연산이라는 말이 자주 나옵니다. 이게 뭐냐면, 스트림에서 데이터를 다루는 과정에서 어디까지 준비 단계인지, 어디서 실제 결과를 만드는지를 구분한 거예요. 쉽게 말해, "준비 작업(중간 연산)"과 "마무리 작업(종단 연산)"의 차이입니다.
1. 스트림(Stream)이란?
이미 알고계시겠지만 스트림은 데이터를 다루는 흐름입니다.
- 예를 들어, "리스트나 배열 같은 데이터 모음을 일렬로 나열해서 처리하는 도구"라고 보시면 좋습니다.
- 스트림에서는 데이터를 "흘려보내며(filter, map)" 처리하고, 최종적으로 "결과를 만들어내거나(forEach, collect)" 끝냅니다.
2. 중간 연산이란?
중간 연산은 데이터를 가공하는 "준비 작업"입니다.
- 이 단계에서는 결과를 바로 내지 않고, 스트림을 그대로 유지합니다. (즉, 아직 결과가 안 만들어진 상태)
- ❗️주의 : 중간 연산만 있다면 스트림은 동작하지 않습니다.
- 중간 연산은 체이닝(chaining)으로 계속 연결할 수 있습니다.
중간 연산의 특징
- 데이터를 필터링하거나(map, filter 등) 변환하는 데 사용됩니다.
- 스트림을 그대로 반환하니까 다른 중간 연산과 연결할 수 있습니다.
- lazy해서 종단 연산이 호출될 때까지 실제로 아무 작업도 하지 않습니다.
💪 자주 쓰는 중간 연산들
📌 filter: 조건에 맞는 데이터만 남기기
List<String> names = List.of("Alice", "Bob", "Charlie");
names.stream()
.filter(name -> name.startsWith("A")); // "Alice"만 남김
📌 map: 데이터를 변환하기
names.stream()
.map(String::toUpperCase); // 모든 이름을 대문자로 변환
📌 sorted: 정렬하기
names.stream()
.sorted(); // 알파벳 순으로 정렬
📌 distinct: 중복을 제거합니다
List<Integer> numbers = List.of(1, 2, 2, 3, 4, 4, 5);
List<Integer> uniqueNumbers = numbers.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueNumbers); // 출력: [1, 2, 3, 4, 5]
📌 limit: 스트림 요소를 처음부터 지정한 개수만큼 가져옵니다
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
List<Integer> limitedNumbers = numbers.stream()
.limit(3)
.collect(Collectors.toList());
System.out.println(limitedNumbers); // 출력: [1, 2, 3]
📌 skip: 처음부터 지정한 개수만큼 요소를 건너뜁니다
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
List<Integer> skippedNumbers = numbers.stream()
.skip(2)
.collect(Collectors.toList());
System.out.println(skippedNumbers); // 출력: [3, 4, 5]
📌 flatMap: 중첩된 스트림을 하나의 스트림으로 펼칩니다
List<List<String>> nestedList = List.of(
List.of("A", "B"),
List.of("C", "D"),
List.of("E", "F")
);
List<String> flattenedList = nestedList.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
System.out.println(flattenedList); // 출력: [A, B, C, D, E, F]
📌 peek: 각 요소를 중간에 들여다보고 싶을 때 사용합니다
List<String> names = List.of("Alice", "Bob", "Charlie");
List<String> result = names.stream()
.filter(name -> name.length() > 3)
.peek(name -> System.out.println("Filtered: " + name))
.collect(Collectors.toList());
// 출력:
// Filtered: Alice
// Filtered: Charlie
3. 종단 연산이란?
종단 연산은 스트림 작업을 끝내고 결과를 만드는 마무리 작업입니다.
- 여기서 스트림 데이터를 소비하고, 결과를 반환하거나 출력합니다.
- 종단 연산이 호출되면, 그제서야 중간 연산이 실제로 실행됩니다.
종단 연산의 특징
- 스트림을 종료시킵니다. (즉, 스트림을 다시 사용할 수 없습니다.)
- 데이터를 출력하거나, 리스트로 모으거나, 합계를 계산하는 등의 작업이 여기서 이루어집니다.
자주 쓰는 종단 연산들
📌 collect: 스트림 데이터를 리스트나 맵으로 모으기
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList()); // 리스트로 변환
📌 forEach: 데이터를 하나씩 처리하기
names.stream()
.forEach(System.out::println); // 스트림 데이터를 출력
📌count: 스트림 데이터 개수 세기
long count = names.stream()
.filter(name -> name.startsWith("A"))
.count(); // 조건에 맞는 데이터 개수 반환
📌 reduce: 데이터를 하나로 합치기
int sum = List.of(1, 2, 3, 4).stream()
.reduce(0, Integer::sum); // 합계 계산
📌 toList: 스트림 데이터를 list로 모아줍니다.
List<String> names = List.of("Alice", "Bob", "Charlie");
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A"))
.toList(); // Java 16부터 추가된 간단한 방법
📌 findAny: 스트림에서 아무 요소나 하나를 반환합니다. (Optional로 반환)
- 순차 스트림: 조건을 만족하는 첫 번째 요소 반환
- 병렬 스트림: 조건을 만족하는 아무 요소나 반환 가능
Optional<String> anyName = names.stream()
.filter(name -> name.length() > 3)
.findAny();
📌 min / max: 스트림에서 최소값이나 최대값을 찾습니다. (Optional로 반환)
Optional<String> shortestName = names.stream()
.min(Comparator.comparing(String::length));
📌 toArray(): 스트림 데이터를 배열로 변환합니다.
List<String> names = List.of("Alice", "Bob", "Charlie");
String[] nameArray = names.stream().toArray(String[]::new);
System.out.println(Arrays.toString(nameArray)); // 결과: ["Alice", "Bob", "Charlie"]
📌 anyMatch / allMatch / noneMatch: 스트림 데이터가 특정 조건에 부합하는지 확인합니다.
boolean anyMatch = names.stream().anyMatch(name -> name.startsWith("A")); // 하나라도 "A"로 시작?
📌 findFirst: 스트림에서 첫 번째 요소를 반환합니다. (Optional로 반환)
Optional<String> firstName = names.stream()
.filter(name -> name.startsWith("B"))
.findFirst();
자주 쓰이는 종단 연산 한눈에 보기
메서드 | 설명 | 반환 타입 |
toList() | 데이터를 리스트로 모으기 | List<T> |
collect() | 데이터를 모아서 원하는 형태로 변환 | Collector<T, A, R> |
forEach() | 데이터를 하나씩 처리 | void |
count() | 데이터 개수 세기 | long |
findFirst() | 첫 번째 데이터 반환 | Optional<T> |
findAny() | 조건에 맞는 아무 데이터 반환 | Optional<T> |
reduce() | 데이터 하나로 축소 | Optional<T> 또는 T |
min() / max() | 최소값/최대값 찾기 | Optional<T> |
anyMatch() | 조건에 맞는 데이터가 있는지 확인 | boolean |
toArray() | 데이터를 배열로 변환 | T[] |
4. 중간 연산과 종단 연산의 연결
중간 연산과 종단 연산은 이런 식으로 연결됩니다.
List<String> names = List.of("Alice", "Bob", "Charlie", "Andrew");
List<String> result = names.stream() // 스트림 생성
.filter(name -> name.startsWith("A")) // 중간 연산
.map(String::toUpperCase) // 중간 연산
.sorted() // 중간 연산
.collect(Collectors.toList()); // 종단 연산
System.out.println(result); // 결과: ["ALICE", "ANDREW"]
5. 중간 연산 vs 종단 연산
역할 | 데이터를 가공/변환하는 준비 단계 | 스트림 작업을 종료하고 결과 생성 |
작동 방식 | 스트림 반환, 체이닝 가능 | 스트림 소모, 결과 반환 또는 출력 |
실행 시점 | 종단 연산이 호출되기 전까지 실행 안 함 | 호출 즉시 실행 |
예시 메서드 | filter, map, sorted | collect, forEach, reduce |
📚 한 줄 요약
- 중간 연산: "필터, 변환, 정렬 같은 준비 작업만 해둔다."
- 종단 연산: "결과를 만들어 스트림을 끝낸다."
- 중간 연산은 체이닝을 통해 여러개를 사용할 수 있지만 종단 연산은 딱 한 번만 사용이 가능합니다.
- 종단 연산만은 사용할 수 있지만 중간 연산만 사용할 수는 없다. 반드시 종단 연산이 있어야 스트림은 동작한다.
'JAVA > 개념' 카테고리의 다른 글
언제 equals를 재정의 해야할까? (0) | 2024.11.22 |
---|---|
Collection.forEach vs Stream.forEach (1) | 2024.11.20 |
stream.forEach (feat. 종단 연산) (1) | 2024.11.18 |
자바 정적 팩토리 메서드 쉽게 이해하기 - 왜 사용하고 어떻게 활용할까? (0) | 2024.10.24 |