참고: 이 섹션의 개념을 더 잘 이해하려면 람다 표현식(Lambda Expressions) 및 메서드 참조(Method References) 섹션을 검토하세요.
컬렉션을 어떤 용도로 사용하시나요? 단순히 개체를 컬렉션에 저장하고 그대로 두는 것이 아닙니다. 대부분의 경우 컬렉션을 사용하여 컬렉션에 저장된 항목을 검색(retrieve)합니다.
람다 표현식 섹션에 설명된 시나리오를 다시 고려해보세요. 소셜 네트워킹 애플리케이션을 만들고 있다고 가정해 보겠습니다. 관리자가 특정 기준을 충족하는 소셜 네트워킹 애플리케이션의 구성원에 대해 메시지 보내기와 같은 모든 종류의 작업을 수행할 수 있는 기능을 만들고 싶습니다.
이전과 마찬가지로 이 소셜 네트워킹 애플리케이션의 구성원이 다음 Person 클래스로 표시된다고 가정합니다.
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
// ...
public int getAge() {
// ...
}
public String getName() {
// ...
}
}
다음 예에서는 for-each 루프를 사용하여 컬렉션 roster(명단)에 포함된 모든 멤버의 이름을 인쇄합니다.
for (Person p : roster) {
System.out.println(p.getName());
}
다음 예에서는 컬렉션 명단에 포함되어 있지만 forEach 집계 작업을 사용하여 모든 멤버를 인쇄합니다.
roster.stream().forEach(e -> System.out.println(e.getName());
이 예에서는 집계 작업을 사용하는 버전이 for-each 루프를 사용하는 버전보다 길지만 더 복잡한 작업에서는 bulk-data 작업을 사용하는 버전이 더 간결하다는 것을 알 수 있습니다.
다루는 주제는 다음과 같습니다.
BulkDataOperationsExamples 예제에서 이 섹션에 설명된 코드 발췌문을 찾아보세요.
파이프라인은 일련(sequence)의 집계(aggregate) 작업입니다. 다음 예에서는 집계 작업 filter와 forEach로 구성된 파이프라인을 사용하여 컬렉션 roster에 포함된 남성 멤버를 인쇄합니다.
roster.stream().filter(e -> e.getGender() == Person.Sex.MALE).forEach(e -> System.out.println(e.getName()));
이 예를 for-each 루프를 사용하여 컬렉션 roster에 포함된 남성 구성원을 인쇄하는 다음 예와 비교하세요.
for (Person p : roster) {
if (p.getGender() == Person.Sex.MALE) {
System.out.println(p.getName());
}
}
파이프라인에는 다음 구성요소가 포함됩니다.
소스(source): 컬렉션, 배열, generator function 또는 I/O 채널이 될 수 있습니다. 이 예에서 소스는 컬렉션 roster입니다.
0개 이상의 중간 작업(intermediate operations). filter와 같은 중간 작업은 새 스트림을 생성합니다.
stream은 일련(sequence)의 요소입니다. 컬렉션과 달리 요소를 저장하는 데이터 구조(data structure)가 아닙니다. 대신 스트림은 파이프라인을 통해 소스의 값을 전달(carries values)합니다. 이 예에서는 stream 메소드를 호출하여 컬렉션 roster에서 stream을 생성합니다.
filter 작업은 해당 조건자(predicate)(이 작업의 매개 변수)와 일치하는 요소가 포함된 새 스트림을 반환합니다. 이 예에서 조건자(predicate)는 람다 식(lambda expression) e -> e.getGender() == Person.Sex.MALE입니다. 객체 e의 성별 필드에 Person.Sex.MALE 값이 있는 경우 부울 값 true를 반환합니다. 결과적으로 이 예의 filter 작업은 컬렉션 roster의 모든 남성 구성원을 포함하는 스트림을 반환합니다.
forEach와 같은 터미널 작업은 기본(primitive) 값(예: double 값), 컬렉션과 같은 비스트림(non-stream) 결과를 생성하거나 forEach의 경우 값이 전혀 없는(no value) 결과를 생성합니다. 이 예에서 forEach 작업의 매개 변수는 객체 e에서 getName 메서드를 호출하는 람다 식 e -> System.out.println(e.getName())입니다. (Java 런타임과 컴파일러는 객체 e의 유형이 Person이라고 추론(infer)합니다.)다음 예에서는 집계 작업 filter, mapToInt 및 average으로 구성된 파이프라인을 사용하여 컬렉션 roster에 포함된 모든 남성 구성원의 평균 연령을 계산합니다.
double average = roster.stream().filter(p -> p.getGender() == Person.Sex.MALE).mapToInt(Person::getAge).average().getAsDouble();
mapToInt 작업은 IntStream 유형(정수 값만 포함하는 스트림)의 새 스트림을 반환합니다. 이 작업은 해당 매개변수에 지정된 함수를 특정 스트림의 각 요소에 적용합니다. 이 예에서 함수는 구성원의 나이를 반환하는 메서드 참조인 Person::getAge입니다. (또는 람다 식 e -> e.getAge()를 사용할 수도 있습니다.) 결과적으로 이 예제의 mapToInt 작업은 컬렉션 roster에 있는 모든 남성 구성원의 연령이 포함된 스트림을 반환합니다.
average 연산은 IntStream 유형의 스트림에 포함된 요소의 평균 값을 계산합니다. OptionalDouble 유형의 객체를 반환합니다. 스트림에 요소가 없으면 average 연산은 OptionalDouble의 빈 인스턴스를 반환하고 getAsDouble 메서드를 호출하면 NoSuchElementException이 발생합니다. JDK에는 스트림의 내용을 결합하여 하나의 값을 반환하는 average과 같은 많은 터미널 작업이 포함되어 있습니다. 이러한 작업을 축소 작업(reduction operation)이라고 합니다. 자세한 내용은 Reduction 섹션을 참조하세요.
forEach와 같은 집계 작업은 반복자와 비슷해 보입니다. 그러나 몇 가지 근본적인 차이점이 있습니다.
내부 반복(iteration)을 사용합니다. 집계 작업에는 컬렉션의 다음 요소를 처리하도록 지시하는 next와 같은 메서드가 포함되어 있지 않습니다. 내부 위임(internal delegation)을 사용하면 애플리케이션이 반복할 컬렉션(what collection it iterates)을 결정하지만 JDK는 컬렉션을 반복하는 방법(how)을 결정합니다. 외부 반복(external iteration)을 사용하면 애플리케이션에서 반복할 컬렉션과 반복 방법을 모두 결정합니다. 그러나 외부 반복은 컬렉션의 요소에 대해서만 순차적으로(sequentially) 반복할 수 있습니다. 내부 반복에는 이러한 제한이 없습니다. 문제를 하위 문제(subproblems)로 나누고 해당 문제를 동시에(simultaneously) 해결한 다음 솔루션 결과를 하위 문제에 결합하는 병렬 컴퓨팅(parallel computing)을 보다 쉽게 활용할 수 있습니다. 자세한 내용은 Parallellism 섹션을 참조하세요.
스트림의 요소를 처리합니다. 집계 작업은 컬렉션에서 직접 처리하는 것이 아니라 스트림에서 요소를 처리합니다. 결과적으로 스트림 작업이라고도 합니다.
동작을 매개변수로 지원합니다. 대부분의 집계 작업에 대한 매개변수로 람다 식을 지정할 수 있습니다. 이를 통해 특정 집계 작업의 동작을 사용자 지정할 수 있습니다.