JavaMedium#059
Java 8의 Stream API를 설명하고 주요 연산들을 예시와 함께 설명해주세요.
#Java#Stream#함수형#Java8
답변 포인트
중간 연산과 최종 연산, 지연 평가(lazy evaluation)를 생각해보세요.
정답 및 해설
빠른 요약
Stream API는 컬렉션을 선언적으로 처리하는 함수형 API입니다. 중간 연산 (lazy, 반환값 Stream): - filter(predicate): 조건에 맞는 요소만 - map(function): 요소 변환 - flatMap: 중첩 스트림 평탄화 - sorted, distinct, limit, skip 최종 연산 (eager, 스트림 소비): - collect(Collectors.toList()) - forEach, count, anyMatch, allMatch - reduce: 누적 계산 - findFirst, min, max 지연 평가: 최종 연산이 호출될 때 중간 연산이 실행됩니다.
Stream API는 Java 8에서 도입된 컬렉션을 선언적(declarative)으로 처리하는 함수형 API입니다. 반복문을 직접 작성하는 대신, 무엇을 할지를 표현하여 코드의 가독성과 유지보수성을 높입니다.
Stream의 특징
- 선언적: 어떻게(how)가 아닌 무엇을(what) 처리할지 표현
- 지연 평가(Lazy Evaluation): 최종 연산이 호출될 때까지 중간 연산이 실행되지 않음
- 일회성: 한 번 소비(최종 연산)된 스트림은 재사용 불가
- 원본 불변: 원본 컬렉션을 변경하지 않음
- 병렬 처리:
parallelStream()으로 간단히 병렬화 가능
Java
// 명령형 방식 (for-loop)
List<String> result = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A")) {
result.add(name.toUpperCase());
}
}
// Stream 방식 (선언적)
List<String> result = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.collect(Collectors.toList());스트림 생성
Java
// 컬렉션으로부터
List<String> list = List.of("a", "b", "c");
Stream<String> streamFromList = list.stream();
// 배열로부터
String[] array = {"a", "b", "c"};
Stream<String> streamFromArray = Arrays.stream(array);
// 직접 생성
Stream<String> streamOf = Stream.of("a", "b", "c");
// 범위 (정수)
IntStream range = IntStream.range(1, 6); // 1, 2, 3, 4, 5
IntStream rangeClosed = IntStream.rangeClosed(1, 5); // 1, 2, 3, 4, 5
// 무한 스트림
Stream<Integer> iterate = Stream.iterate(0, n -> n + 2).limit(5); // 0, 2, 4, 6, 8
Stream<Double> generate = Stream.generate(Math::random).limit(3);중간 연산 (Intermediate Operations)
중간 연산은 항상 스트림을 반환하며, 지연(lazy) 평가됩니다. 여러 개를 체이닝할 수 있습니다.
filter — 조건에 맞는 요소만 선택
Java
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> evens = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
// [2, 4, 6, 8, 10]map — 요소를 다른 타입/값으로 변환
Java
List<String> names = List.of("alice", "bob", "charlie");
List<String> upperNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// [ALICE, BOB, CHARLIE]
List<Integer> nameLengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
// [5, 3, 7]flatMap — 중첩 스트림을 평탄화
Java
List<List<Integer>> nested = List.of(
List.of(1, 2, 3),
List.of(4, 5),
List.of(6, 7, 8, 9)
);
List<Integer> flat = nested.stream()
.flatMap(Collection::stream) // 각 리스트를 스트림으로 펼침
.collect(Collectors.toList());
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 단어 분리 예시
List<String> sentences = List.of("Hello World", "Java Stream");
List<String> words = sentences.stream()
.flatMap(s -> Arrays.stream(s.split(" ")))
.collect(Collectors.toList());
// [Hello, World, Java, Stream]sorted — 정렬
Java
List<String> names = List.of("banana", "apple", "cherry");
List<String> sorted = names.stream()
.sorted() // 자연 순서
.collect(Collectors.toList());
// [apple, banana, cherry]
List<String> sortedByLength = names.stream()
.sorted(Comparator.comparingInt(String::length))
.collect(Collectors.toList());
// [apple, banana, cherry] → 길이 기준distinct, limit, skip
Java
List<Integer> nums = List.of(1, 2, 2, 3, 3, 3, 4);
List<Integer> unique = nums.stream()
.distinct()
.collect(Collectors.toList());
// [1, 2, 3, 4]
List<Integer> firstThree = nums.stream()
.limit(3)
.collect(Collectors.toList());
// [1, 2, 2]
List<Integer> afterTwo = nums.stream()
.skip(2)
.collect(Collectors.toList());
// [2, 3, 3, 3, 4]최종 연산 (Terminal Operations)
최종 연산은 스트림을 **소비(consume)**하고 결과를 반환합니다. 호출 후 스트림 재사용 불가합니다.
collect — 결과를 컬렉션으로 수집
Java
List<String> names = List.of("Alice", "Bob", "Charlie", "Alice");
// List로 수집
List<String> list = names.stream().collect(Collectors.toList());
// Set으로 수집 (중복 제거)
Set<String> set = names.stream().collect(Collectors.toSet());
// Map으로 수집
Map<String, Integer> nameToLength = names.stream()
.distinct()
.collect(Collectors.toMap(
name -> name, // 키 매핑
String::length // 값 매핑
));
// {Alice=5, Bob=3, Charlie=7}
// 그룹화
Map<Integer, List<String>> byLength = names.stream()
.collect(Collectors.groupingBy(String::length));
// {3=[Bob], 5=[Alice, Alice], 7=[Charlie]}
// 문자열 조인
String joined = names.stream()
.collect(Collectors.joining(", ", "[", "]"));
// [Alice, Bob, Charlie, Alice]forEach, count, anyMatch, allMatch, noneMatch
Java
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// forEach
numbers.stream().forEach(System.out::println);
// count
long count = numbers.stream().filter(n -> n > 3).count(); // 2
// anyMatch — 하나라도 조건 만족
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0); // true
// allMatch — 모두 조건 만족
boolean allPositive = numbers.stream().allMatch(n -> n > 0); // true
// noneMatch — 아무것도 조건 만족 안 함
boolean noneNegative = numbers.stream().noneMatch(n -> n < 0); // truereduce — 요소를 하나의 값으로 축소
Java
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// 합산
int sum = numbers.stream().reduce(0, Integer::sum); // 15
// 최대값
Optional<Integer> max = numbers.stream().reduce(Integer::max); // Optional[5]
// 문자열 결합
List<String> words = List.of("Java", "is", "cool");
String sentence = words.stream().reduce("", (a, b) -> a + " " + b).strip();
// "Java is cool"findFirst, min, max
Java
List<String> names = List.of("Charlie", "Alice", "Bob");
Optional<String> first = names.stream()
.filter(n -> n.startsWith("A"))
.findFirst(); // Optional[Alice]
Optional<String> shortest = names.stream()
.min(Comparator.comparingInt(String::length)); // Optional[Bob]
Optional<String> longest = names.stream()
.max(Comparator.comparingInt(String::length)); // Optional[Charlie]지연 평가 (Lazy Evaluation) 예시
Java
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// 최종 연산이 없으면 아무것도 실행되지 않음
Stream<Integer> stream = numbers.stream()
.filter(n -> {
System.out.println("filter: " + n);
return n % 2 == 0;
})
.map(n -> {
System.out.println("map: " + n);
return n * 10;
});
// 여기까지 아무 출력 없음!
List<Integer> result = stream.collect(Collectors.toList());
// 최종 연산 호출 시 비로소 실행됨
// filter: 1, filter: 2, map: 2, filter: 3, filter: 4, map: 4, filter: 5
// result: [20, 40]실전 활용 예시
Java
// 직원 목록에서 부서별 평균 연봉 계산
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
// 상위 3명의 고연봉자 이름 조회
List<String> top3 = employees.stream()
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
.limit(3)
.map(Employee::getName)
.collect(Collectors.toList());
// 전체 급여 합산
double totalSalary = employees.stream()
.mapToDouble(Employee::getSalary)
.sum();중간 연산 vs 최종 연산 정리
| 구분 | 연산 | 반환 타입 | 실행 시점 |
|---|---|---|---|
| 중간 연산 | filter, map, flatMap, sorted, distinct, limit, skip | Stream | lazy (지연) |
| 최종 연산 | collect, forEach, count, reduce, findFirst, anyMatch, allMatch, min, max | 결과값 | eager (즉시) |