이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › Java › 람다와 스트림
스트림으로 모으기
스트림의 결과를 어떤 모양으로 모으나
스트림에서 흘러나온 원소들은 마지막에 collect 같은 최종 연산으로 결과를 받아야 쓸 수 있습니다. 어떤 Collector를 주느냐에 따라 List가 되기도 하고, 키별로 묶인 Map이 되기도 하고, 문자열 하나가 되기도 합니다.
실행으로 대조 · JDK 21에서 스물네 가지 조합을 실행해 대조
이 실습에 나오는 개념
- Stream API
- 컬렉션 등의 데이터에 대해 필터링, 변환, 집계를 선언적으로 표현하는 API다. filter, map 같은 중간 연산을 이어 붙이고 collect, forEach 같은 최종 연산으로 마무리한다. 중간 연산은 최종 연산이 호출될 때까지 실행되지 않으며(지연 평가), 스트림은 원본 데이터를 변경하지 않고 한 번만 사용할 수 있다.
- Collector와 collect
- collect는 스트림에서 흘러나온 원소들을 하나의 결과로 모으는 최종 연산이고, Collector는 그 모으는 방법(어떤 그릇에 어떻게 담을지)을 나타내는 객체다. Collectors 클래스에 자주 쓰는 Collector가 준비되어 있다. toList와 toSet은 컬렉션에 담고, joining은 문자열 하나로 잇고, toMap은 키와 값을 정해 Map에 담는다. toMap은 같은 키가 두 번 나오면 IllegalStateException을 던지므로, 키가 겹칠 수 있으면 두 값을 합치는 병합 함수를 세 번째 인자로 준다.
- groupingBy와 partitioningBy
- 스트림의 원소를 무리로 나누어 Map으로 모으는 Collector다. groupingBy는 원소마다 키를 계산해 키가 같은 원소끼리 묶으며, 기본 결과는 Map<키, List<원소>>이다. 두 번째 인자로 counting, summingInt 같은 Collector를 주면 목록 대신 무리별 개수나 합계를 얻는다. partitioningBy는 조건 하나로 true 무리와 false 무리 둘로 나누며, 해당하는 원소가 없어도 두 키가 항상 들어 있다. groupingBy가 돌려주는 Map은 순서를 보장하지 않으므로, 순서가 필요하면 TreeMap 같은 Map을 쓰도록 따로 지정한다.
- 메서드 참조
- 이미 있는 메서드를 람다 대신 이름으로 가리키는 문법이다. String::length, System.out::println처럼 클래스(또는 객체)와 메서드 이름을 ::로 연결해 쓴다. 람다가 단순히 메서드 하나를 호출하기만 할 때 더 짧게 표현할 수 있다.
- List, Set, Map
- 자바 컬렉션의 세 가지 기본 유형이다. List는 순서가 있고 중복을 허용하며 인덱스로 접근한다. Set은 중복을 허용하지 않는다. Map은 키와 값의 쌍을 저장하며 키는 중복될 수 없다. 데이터의 성격(순서 필요 여부, 중복 허용 여부, 키로 찾는지)에 따라 고른다.
이렇게 바꿔 보세요
- 처음 설정으로 — toMap에 같은 고객이 두 번 나옵니다. IllegalStateException이 납니다.
- 병합 함수를 주면 — 같은 키의 값을 더해서 하나로 만듭니다.
- toList로 모으기 — 흘러나온 순서 그대로 List에 담습니다.
- groupingBy로 묶기 — 고객별로 주문 목록이 만들어집니다.
- 묶어서 개수 세기 — 목록 대신 개수만 남깁니다. 값의 타입이 Long입니다.
- partitioningBy로 둘로 나누기 — 조건이 true인 것과 false인 것으로 나눕니다.
- 주문이 없을 때 둘로 나누기 — 원소가 없어도 false와 true 두 칸은 항상 있습니다.
처음 설정으로 실행하면
- orders.stream(): 주문 다섯 개가 kim:300, han:100, kim:200, choi:400, han:500 순서로 하나씩 흘러나오는 스트림을 만듭니다. 아직 아무것도 모으지 않았습니다.
- .collect(Collectors.toMap(Order::customer, Order::price)): collect는 흘러나오는 원소를 하나씩 받아 그릇에 담는 최종 연산이고, 어떤 그릇에 어떻게 담을지는 괄호 안의 Collector가 정합니다. toMap은 빈 Map을 준비합니다. 원소마다 customer를 키로, price를 값으로 넣습니다. 한 키에 값은 하나만 들어갑니다.
- kim:300 주문: 키 kim, 값 300을 Map에 넣습니다.
- han:100 주문: 키 han, 값 100을 Map에 넣습니다.
- kim:200 주문: 같은 키(kim)가 이미 Map에 있습니다. 이미 있는 값 300과 새 값 200 가운데 무엇을 남길지 정해 주지 않았으므로 IllegalStateException이 나고 프로그램이 여기서 멈춥니다. 예외 메시지는 "Duplicate key kim (attempted merging values 300 and 200)"입니다.
실행 결과
- toMap은 한 키에 값 하나만 담습니다. 키가 겹칠 수 있는 데이터라면 세 번째 인자로 병합 함수를 주거나, 겹치는 것을 목록으로 모으는 groupingBy를 써야 합니다.
코드
record Order(String customer, int price) {
@Override
public String toString() { return customer + ":" + price; } // 짧게 출력하려고
}
List<Order> orders = List.of(
new Order("kim", 300), new Order("han", 100), new Order("kim", 200),
new Order("choi", 400), new Order("han", 500));
Map<String, Integer> result = orders.stream()
.collect(Collectors.toMap(Order::customer, Order::price));
System.out.println(result);
이 모형의 전제
스물네 가지 조합의 출력과 예외(종류와 메시지)를 이 PC의 JDK 21에서 같은 코드를 실행한 결과와 대조했습니다. 결과의 타입은 그 타입으로 선언한 코드가 실제로 컴파일되는지로 확인했고, counting()의 결과를 Map<String, Integer>로 받으면 컴파일되지 않는 것도 확인했습니다. groupingBy와 toMap이 돌려주는 Map은 순서를 보장하지 않습니다(JDK 21에서는 HashMap). 이 실습은 Map을 키의 사전 순서로 정리해 보여 주며, 여기에 쓴 고객 이름들은 JDK 21의 실제 출력 순서도 그와 같았습니다. 다른 이름을 쓰면 실제 출력 순서는 달라질 수 있습니다. 예외 메시지의 글귀는 JDK 버전에 따라 다를 수 있습니다. 스트림은 원소를 하나씩 차례로 처리하는 경우(병렬이 아닌 스트림)만 다룹니다.
앞 실습: 스트림 지연 평가 · 다음 실습: 병렬 스트림과 공용 풀