Step 1: "반복문, 이제 그만 놓아주세요" (스트림의 등장)
List<Idol> idols = List.of(
new Idol("장원영", "IVE", 100),
new Idol("안유진", "IVE", 95),
new Idol("카즈하", "LE SSERAFIM", 90),
new Idol("김채원", "LE SSERAFIM", 92),
new Idol("닝닝", "AESPA", 88)
);
// 1. 임시 저장소 만들기 (귀찮음)
List<Idol> iveMembers = new ArrayList<>();
// 2. 반복문 + 조건문 지옥 (어떻게 할 것인가?)
for (Idol idol : idols) {
if ("IVE".equals(idol.getTeam())) {
iveMembers.add(idol);
}
}
// 3. 정렬하기 (코드가 너무 길어져서 생략...)
// Collections.sort(iveMembers, ...);
// 4. 또 반복문 돌려서 이름 뽑기
List<String> resultNames = new ArrayList<>();
for (Idol idol : iveMembers) {
resultNames.add(idol.getName());
}
List<String> result = idols.stream()
.filter(idol -> "IVE".equals(idol.getTeam())) // 1. IVE만 남겨라 (Filtering)
.sorted(Comparator.comparing(Idol::getScore)) // 2. 점수대로 줄 세워라 (Sorting)
.map(Idol::getName) // 3. 이름표만 떼라 (Mapping)
.collect(Collectors.toList()); // 4. 바구니에 담아라 (Collecting)
스트림 코드는 항상 이 3박자를 따릅니다.
생성하기: idols.stream() (데이터의 흐름을 만든다)
가공하기 (중간 연산): filter, sorted, map (데이터를 깎고 다듬는다)
결과 만들기 (최종 연산): collect (최종 결과물을 포장한다)
Step 2: "데이터 공장의 기계들"
(중간 연산)
filter: "입구 컷!"조건에 맞지 않는 데이터는 가차 없이 탈락시킵니다. if문 대신 씁니다.
// 점수가 90점 이상인 아이돌만 통과!
idols.stream()
.filter(idol -> idol.getScore() >= 90)
...
distinct: "중복은 안 받아요"똑같은 데이터가 여러 개 들어오면 하나만 남깁니다.
⚠️ 주의: 이게 작동하려면 Idol 클래스에 equals()와 hashCode()가 제대로 구현되어 있어야 합니다! (주민등록증 확인 같은 거죠)
idols.stream()
.distinct() // 쌍둥이(중복 객체) 하나 제거
...
limit: "선착순!"데이터가 아무리 많아도 딱 정해진 개수만큼만 통과시킵니다.
idols.stream()
.limit(2) // "앞에 2명만 들어오세요~"
...
map: "변신!"데이터의 형태를 바꿉니다. 사과를 주면 사과 주스로, 아이돌 객체를 주면 이름(String)으로!
가장 많이 쓰이는 연산 중 하나입니다.
// Idol 객체 -> String(이름 + "님")으로 변환
idols.stream()
.map(idol -> idol.getName() + "님")
...
sorted: "줄 서세요"데이터를 정해진 기준에 따라 정렬합니다.
// 람다를 쓰면 정렬도 한 줄 컷!
idols.stream()
.sorted(Comparator.comparingInt(Idol::getScore)) // 점수 낮은 순
...
List<String> result = idols.stream()
.filter(idol -> idol.getScore() >= 90) // 1. 우등생 필터링
.distinct() // 2. 중복 제거
.sorted(Comparator.comparing(Idol::getName)) // 3. 이름순 정렬
.map(Idol::getName) // 4. 이름만 추출
.limit(2) // 5. 2명만 제한
.collect(Collectors.toList()); // 6. 포장 (최종 연산)
Step 3: "결과의 미학" (최종 연산)
int totalScore = idols.stream()
.map(Idol::getScore) // 1. 점수만 추출
.reduce(0, (a, b) -> a + b); // 2. 0부터 시작해서 계속 더하기
// Map<String, List<Idol>> 형태로 반환
Map<String, List<Idol>> byTeam = idols.stream()
.collect(Collectors.groupingBy(Idol::getTeam));
[결과]
IVE: [장원영, 안유진, 레이]
Aespa: [카리나, 지젤]
LE SSERAFIM: [김채원, 카즈하, 은채]
// Map<Boolean, List<Idol>> 형태로 반환
Map<Boolean, List<Idol>> isGenius = idols.stream()
.collect(Collectors.partitioningBy(idol -> idol.getScore() >= 90));
[결과]
true (우등생): [장원영, 안유진, 카리나...]
false (노력파): [은채...]
Step 4: "한 번 더, 스트림!”
// 미션: "300칼로리보다 높은 요리 중, 처음 2개는 건너뛰고 3개만 보여줘!"
menuList.stream()
.filter(m -> m.getCalories() > 300) // 1. 300칼로리 초과 필터
.skip(2) // 2. 앞의 2개는 패스
.limit(3) // 3. 딱 3개만 선정
.forEach(System.out::println);
// 미션: "500칼로리 이상 메뉴들의 이름만 뽑아서 리스트로 만들기"
List<String> highCalorieNames = menuList.stream()
.filter(m -> m.getCalories() >= 500)
.map(Dish::getName) // 요리 객체 -> 이름(String)으로 변환!
.collect(toList());
// 미션: "요리의 총 칼로리 합계 구하기" (숫자 전용 스트림 사용)
int totalCalories = menuList.stream()
.mapToInt(Dish::getCalories) // IntStream으로 변환하여 최적화
.sum();
// 채식주의자가 먹을 수 있는 요리가 하나라도 있나요?
boolean hasVeggie = menuList.stream()
.anyMatch(Dish::isVegetarian); // 결과: true
// 모든 요리가 1000칼로리 미만인가요?
boolean isHealthy = menuList.stream()
.allMatch(m -> m.getCalories() < 1000); // 결과: true
// 미션: "육류 요리 중 칼로리가 낮은 순으로 정렬하기"
menuList.stream()
.filter(m -> m.getType() == DishType.MEAT)
.sorted(Comparator.comparing(Dish::getCalories)) // 오름차순
.forEach(System.out::println);
// 미션: "이름을 역순(Z-A)으로 정렬하기"
menuList.stream()
.sorted(Comparator.comparing(Dish::getName).reversed()) // 내림차순
.forEach(System.out::println);
// 하 1. 연도별 필터링:2022년에 예약된 모든 여행 정보를 찾아 가격 오름차순으로 정렬하여 출력하세요.
List<Booking> list = bookings.stream()
.filter(b -> b.getYear() == 2022)
.sorted(Comparator.comparingDouble(Booking -> Booking.getPrice()))
.toList();// Booking::getPrice
for (Booking booking : list) {
System.out.println(booking);
}
System.out.println("---------------------------------");
// 하 2. 고유값 추출:여행지가 포함된 모든 국가(Country)의 이름을 중복 없이 리스트로 만드세요.
List<String> Country = bookings.stream()
.map(b -> b.getDestination().getCity())
.distinct()
.collect(Collectors.toList());
System.out.println(Country);
System.out.println("---------------------------------");
// 하 3. 데이터 변환:모든 여행자의 이메일 주소를 수집하여 알파벳 순서대로 정렬된 리스트를 반환하세요.
List<String> Email = bookings.stream()
.map(b -> b.getTraveler().getEmail())
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(Email);
System.out.println("---------------------------------");
// 하 4. 단순 존재 확인:이탈리아(Italy)로 가는 여행 예약이 한 건이라도 있는지boolean값으로 확인하세요.
boolean b1 = bookings.stream()
.anyMatch(b -> "Italy".equals(b.getDestination().getCountry()));
System.out.println(b1);
System.out.println("---------------------------------");
// 중 1. 조건부 매핑과 정렬:'Paris'로 예약된 모든 여행자를 찾아
// 이름순으로 오름차순 정렬하여 출력하세요. (중복된 여행자는 한 번만 나오게 하세요.)
List<Booking> collect = bookings.stream()
.filter(b -> "Paris".equals(b.getDestination().getCity()))
.sorted(Comparator.comparing(b -> b.getTraveler().getName()))
.distinct()
.toList();
for (Booking booking : collect) {
System.out.println(booking);
}
System.out.println("---------------------------------");
// 중 2. 수치 합산:프랑스(France)로 가는 모든 여행의 총 가격 합계를 계산하세요.
double sum = bookings.stream()
.filter(b -> "France".equals(b.getDestination().getCountry()))
.mapToDouble(Booking::getPrice)
.sum();
System.out.println(sum);
System.out.println("---------------------------------");
// 중 3. **최댓값과 최저값:**모든 예약 중 최고 가격을 찾고, 가장 저렴한 여행의 전체 정보를 출력하세요.
// - *주의: 데이터가 하나도 없을 경우를 대비해`orElse()`처리를 고민해 보세요.*
double max = bookings.stream()
.mapToDouble(Booking::getPrice)
.max()
.orElse(0);
System.out.println("최고 가격: " + max);
Booking min = bookings.stream()
.min(Comparator.comparingDouble(Booking::getPrice)) // price 기준 최솟값 Booking 찾기
.orElse(null);
if (min == null) {System.out.println("예약 데이터가 없습니다.");}
else {System.out.println("가장 저렴한 여행 정보: " + min);}
System.out.println("---------------------------------");