본문 바로가기

JAVA/JAVA 8

자바 8 #스트림(Stream) 관련 메서드

반응형

filter, map, flatMap 메서드

스트림 변환은 한 스트림에서 데이터를 읽고, 변환된 데이터를 다른 스트림에 넣는다.

이미 특정 조건과 일치하는 모든 요소를 담은 새로운 스트림을 돌려주는 filter변환


List<String> wordList = ...;

Stream<String> words = worlList.stream();

Stream<String> longWords = words.filter(w -> w.length() > 12);


filter의 인자는 Predicate<T>, 즉 T를 받고 boolean을 리턴하는 함수이다.

스트림에 있는 값들을 특정 방식으로 변환하고 싶을 때는 map메서드를 사용하고 변환을 수행하는 함수를 파라미터로 전달한다.

예) 모든 단어를 소문자로 변환

Stream<String> lowercaseWords = words.map(String::toLowerCase);


예) 각 단어의 첫번째 문자를 포함하는 스트림

Stream<Character> firstChars = words.map(s -> s.charAt(0));


map을 사용하면 함수가 각 요소에 적용되며, 리턴 값들이 새로운 스트림으로 모인다.


예) 

public static Stream<Character> characterStream(String s) {

List<Character> result = new ArrayList<>();

for(char c: s.toCharArray()) result.add(c);

return result.stream();

}


characterStream("boat")는 스트림["b","o","a","t"]를 리턴한다.

이 메서드를 문자열의 스트림에 매핑하자.


Stream<Stream<Character>> result = words.map(w -> characterStream(w));

결과 : [...["y","o","u","r"],["b","o","a","t"], ...]


Stream<Character> letters = words.flatMap(w -> characterStream(w));

// 각 단어를 대상으로 characterStream을 호출하고 결과를 펼친다.

결과 : ["y","o","u","r","b","o","a","t""]


 flatMap은 컴퓨터 과학에서 일반적인 개념이다.

 제네릭 타입 G(예 Stream), 타입 T를 G<U>로 변환하는 함수 f  그리고 타입 U를 G<V>로 변환하는 함수 g가 있다고 하자.

 그러면 flatMap을 사용해서 이 함수들을 합성(compose)할 수 있다.

 즉, 먼저 f를 적용한 후 g를 적용한다. 이는 모나드 이론에서 핵심 개념이다.



서브스트림 추출과 스트림 결합

stream.limt(n) 호출은 n개 요소 이후 끝나는 새로운 스트림을 리턴한다.

무한 스트림을 필요한 크기로 잘나낼 때 유용하다.

예) 난수 100를 포함하는 스트림

 Stream<Double> randoms = Stream.generate(Maht::random).limit(100);


 stream.skip(n) 호출은 반대 작업을 수행

 처음 n개 요소를 버린다.


예) 첫번째 요소를 사라지게 한다.

Stream<String> words = Stream.of(contents.split("[\\P{L}]+")).skip(1);


Stream 클래스의 정적 concat 메서드를 이용하면 두 스트림을 연결할 수 있다.

예) 

Stream<Character> combined = Stream.concat(characterStream("Hello"), characterStream("World"));



peek 메서드는 원본과 동일한 요소들을 포함하는 다른 스트림을 돌려주지만, 전달받은 함수요소 추출시 마다 호출한다.

따라서 디버깅을 수행할 때 유용하다.


Object[] powers = Stream.iterate(1.0, p -> p * 2)

.peek(e -> System.out.println("Fetching " + e))

.limit(20)

.toArray();

요소를 실제 접근할때마다 메시지를 출력한다.

이방법으로 iterate 메서드가 리턴하는 무한 스트림이 지연 처리됨을 확인할 수 있다.


상태유지변환

앞절의 스트림변환은 무상태변환 stateless transformation이다.

필터링 또는 맵핑된 스트림에서 요소를 추출할때 결과가 이전 요소에 의존하지 않는다.

몇가지 상태 유지 변환도 존재한다. 

예) distinct 메서드를 중복을 제거하는점을 제외하면 원본 스트림으로부터 요소들을 같은 순서로 돌려주는 스트림을 리턴한다.

이 경우 스트림은 이미 만난 요소들을 확실히 기억해야 한다.


Stream<String> uniqueWords = Stream.of("merrily", "merrily", "gently").distinct();


sorted 메서드는 요소들을 돌려주기 전에 반드시 전체 스트림을 보고 정렬해야 한다.

(결과로 가장 작은 요소가 마지막 요소일 수 있다.)


sorted 메서드는 여러 버전이 있다.

한 버전은 Comparable 요소들의 스트림을 대상으로 작업하고

다른 버전은 Comparator를 받는다.

문자열을 정렬해서 가장 긴 문자열이 처음에 나타나게 한다.

Stream<String> longestFirst = words.sorted(Comparator.comparing(String::length).reversed());



물론 스트림을 사용하지 않고도 컬렉션을 정렬할 수 있다.

sorted 메서드는 정렬 과정이 스트림 파이프라인 stream pipeline의 일부일때 유용하다.


Collection.sort 메서드는 컬렉션을 직접 정렬한다.

Stream.sorted 메서드는 새롭게 정렬된 스트림을 리턴한다.



단순 리덕션

가장 중요한 부분인 스트림 데이터로부터 결과 얻기

리덕션(환산) reduction 메서드는 스트림을 프로그램에서 사용할 수 있는 값으로 리듀스reduce한다.

리덕션은 최종연산(terminal operation)이다. 최종 연산을 적용한 후에는 스트림을 사용할 수 없다.


단순 리덕션으로 count, max, min 있다.

이들 메서드는 결과를 감싸고 있거나(스트림이 때로는 비어있을수 있기 때문에) 결과가 없음을 나타내는 Optional<T>값을 리턴한다.

예전에는 이러한 상황을 보통 널(null)을 리턴했다.

하지만 널을 리턴하면 완전히 테스트하지 못한 프로그램에서 뜻하지 않는 상황에 널 포인트 예외를 일으킬수 있다.

자바 8에서 Optional 타입은 리턴 값이 빠진 상황을 가리킬때 선호하는 방식이다.


Optional<String> largest = words.max(String::compareToIgnoreCase);

if(largest.isPresent())

System.out.println("largest: " + largest.get());


findFirst 메서드는 비어있지 않는 컬렉션에서 첫번째 값을 리턴한다.

Optional<String> startsWithQ = words.filter(s -> s.startsWith("Q")).findFirst();


첫번재 값은 물론 어떤 일치 결과든 괜찮다면 findAny 메서드를 사용한다.

이 메서드는 병렬화할 때 유용한데, 이 경우 조사 대상 세그먼트(segment)들에서 처음 일치가 발견되면 계산을 완료하기 때문이다.


Optional<String> startsWithQ1 = words.parallel().filter(s -> s.startsWith("Q")).findAny();


단순히 일치하는 요소가 있는지 알고 싶을 경우 anyMatch를 사용한다.

boolean aWordStartsWithQ = words.parallel().anyMatch(s -> s.startsWith("Q"));


모든 요소가 프레디케이트와 일치하거나 아무것도 일치 하지 않을 때 true를 리턴하는 allMatch, noneMatch 메서드를 사용한다.

이들 메서드는 항상 전체 스트림을 검사하지만, 여전히 병렬 실행을 통해 이점을 얻을 수 있다.


옵션타입

Optional<T> 객체는 T 타입 객체 또는 객체가 없는 경우의 래퍼이다.

Optional<T>는 객체 또는 null을 가리키는 T 타입 레퍼런스보다 안전한 대안으로 만들어졌다.

하지만 올바르게 사용할 경우에만 더 안전하다.


get 메서드를 감싸고 있는 요소가 존재할 때는 요소를 얻고, 그렇지 않으면 NoSuchElementException을 던진다.


Optional<T> optionalValue = ...;

optionalValue.get().someMethod();


위의 예제는 다음 예제보다 안전할 것이 없다.


T value = ...;

value.someMethod();


isPresent  메서드는 Optional<T> 객체가 값을 포함하는지 알려준다.

if(optionalValue.isPresent()) optionalValue.get().someMethod();


하지만 앞의 예제가 다음 예제보다 쉽지는 않다.

if(value != null) value.someMethod();


옵셥값 다루기


Optional을 효과적으로 사용하는 핵심은 올바른 값을 소비하거나 대체 값을 생산하는 메서드를 사용하는 것이다.


ifPresent 메서드는 함수를 받는 두번째 형태가 있다.

옵션 값이 존재하면 해당 함수로 전달되며, 그렇지 않으면 아무 일도 일어나지 않는다.

if 문을 사용하는 대신 optionalValue.ifPresent(v -> v 처리);


예) 값이 존재할 경우 집합에 해당 값을 추가하려고 할 때

optionalValue.ifPresent(v -> results.add(v));


optionalValue.ifPresent(results::add);


함수를 받는 ifPresent 버전을 호출할때 값이 리턴되지 않는다.

따라서 결과를 처리하고 싶은 경우에는 대신 map을 사용한다.


Optional<Boolean> added = optionalValue.map(results::add);


added는 세가지 값(optionalValue 가 존재하는 경우 Optional로 감싼 true 또는 false 값, 존재하지 안는경우 빈 Optional) 중 하나를 가진다.


이 map 메서드는 Stream 인터페이스의 map  메서드와 대응한다.

옵션값을 간단하게 크기가  0 또는 1인 스트림으로 생각하자.

결과는 다시 크기 0 또는 1이되며, 결과가 1인 경우 함수가 적용된다


옵션값을 이용하는 다른 전략은 값이 없을 때 대체 값을 생산하는 방법이다.

일치하는 요소가 없을 때 사용할 디폴트(빈 문자열)가 있기 마련이다.


String result = optionalString.orElse("");


디폴트를 계산하는 코드를 호출

String result1 = startsWithQ.orElseGet( () -> System.getProperty("user.dir")); //필요할때만 함수가 호출된다.


또는 다음과 같이 값이 없는 경우 또 다른 예외를 던지고 싶을 수도 있다.


String result2 = startsWithQ.orElseThrow(NoSuchElementException::new); //예외 객체를 돌려주는 메서드를 제공한다.



옵션 값 생성하기

Optional 객체를 생성하는 메서드를 작성할 때 사용할 수 있는 몇가지 정적 메서드가 있다.

Optional.of(result) 또는 Optional.empty()를 이용해 Optional 객체를 생성한다.


public static Optional<Double> inverse(Double x) {

return x == 0 ? Optional.empty() : Optional.of(1/ x);

}


ofNullable 메서드는 null값 사용을 옵션 값 사용으로 이어주는 용도로 만들어졌다.

Optional.ofNullable(obj)는 obj가 null이 아니면 Optional.of(obj)를, null이면 Optional.empty()를 리턴한다.



flatMap을 이용해 옵션 값 함수 합성하기

Optional<T>를 리턴하는 메서드 f가 있고, 대상타입 T는 Optional<U>를 리턴하는 메서드 g를 포함하고 있다고 하자.

일반 메서드라면 s.f().g()를 호출하는 방법으로 이 메서드를 합성할수 있다.

하지만 이경우에는 s.f()에서 T가 아닌 Optional<T> 타입을 리턴하므로, 이러한 합성Composition이 동작하지 않는다.

대신 다음과 같이 호출한다.


Optional<U> result = s.f().flatMap(T::g);


이렇게 하면 s.f()가 존재하면 g가 적용되고, 그렇지 않으면 비어있는 Optional<U>가 리턴된다.


분명, Optional 값을 리턴하는 다른 메서드나 람다가 있다면 이 과정을 반복할 수 있다.

그러면 단순히 호출 flatMap에 연결하는 것만으로 모든 부분이 성공할 경우에만 전체가 성공하는 단계들의 파이프라인을 구축할 수 있다.


예)

public static Optional<Double> squareRoot(Double x) {

return x < 0 ? Optional.empty() : Optional.of(Math.sqrt(x));

}


역수의 루트를 계산할 수 있다.


Optional<Double> result = inverse(x).flatMap(Test::squareRoot);


Optional<Double> result = Optional.of(-4.0).flatMap(Test::inverse).flatMap(Test:squareRoot);

inverse나 squareRoot 메서드 중 하나가 Optional.empty()를 리턴하면 결과는 비어있게 된다.


리덱션 연산

합계를 계산하거나 스트림의 요소들을 다른 방법으로 결합하고 싶은 경우, reduce 메서드들 중 하나를 사용할 수 있다.

가장 단순한 형태는 이항 함수 binary function를 받아서 처음 두 요소부터 시작해서 계속해서 해당 함수를 적용한다.


합계함수

Stream<Integer> values = ...;

Optional<Integer> sum = values.reduece((x, y) -> x + y);


이 경우 reduce 메서드는 u0 + u1 + u2 + ...을 계산하며, 여기서 Ui는 스트림요소들을 나타낸다.

이 메서드는 스트림이 비어있는 경우 유효한 결과가 없으므로 Optional을 리턴한다.


values.reduce((x, y) -> x + y) 대신 values.reduce(Integer::sum)을 사용할 수도 있다.


일반적으로 reduce 메서드가 리덕션 연산 op를 가지면, 해당 리덕션은 u0 op u1 op u2 op ..을 돌려준다.

여기서는 함수 호출 op(ui, ui+1)을 ui op ui+1로 작성했다.

연산은 결합법칙을 지원해야한다. 즉 요소들을 결합하는 순서는 문제가 되지 않아야 한다.

수학에서는 결합법칙을 (x op y) op z = x op (y op z)로 표기한다.

연산이 결합 법칙을 지원하면 병렬 스트림을 통한 효율적인 리덕션이 가능하다.


합계, 곱셈, 문자열 연결, 최대값, 최소값, 합집합, 교집합 등 실전에서 유용한 다양한 결합 연산이 있다.

결합 법칙이 적용되지 않는 연산의 예는 뺄셈으로 (6 - 3) -2 != 6 - ( 3 - 2)이다


e op x = x 같은 항등값(identity) e가 있을 때는 해당요소를 계산의 시작으로 사용할 수 있다.

예) 0은 덧셈의 항등값이다. 


이제 두번째 형태의 reduce를 호출한다.


Stream<Integer> values = ...;

Integer sum = values.reduce(0, (x, y) -> x + y) // 0 + u0 + u1 + u2 + ... 을 계산한다.


스트림이 비어 있으면 항등값을 리턴하므로, 더는 Optional 클래스를 다룰 필요가 없다.


이제 객체 스트림이 있고, 문자열 스트림에서 모든 문자열의 길이 같은 특정 프로퍼티의 합계를 구하려고 한다고 하자.

단순한 형태의 reduce를 사용할 수 없고, 인자들과 결과의 타입이 같은 (T, T) -> T 함수가 필요하다.

하지만 이 상황에서는 두가지 타입이 있다.

다시말해, 스트림 요소들은 String이고, 누적 결과는 정수다.

물론 이 상황을 다룰 수 있는 reduce 형태가 있다.


먼저 '누적' 함수 (total, word) -> total + word.length()를 전달한다.

이 함수는 반복호출되어 누적 합계를 만들어낸다.

하지만 계산을 병렬화하면 이와 같은 계산이 여러개 존재하므로 각각의 결과를 결합해야한다.

따라서 각 부분의 결과를 결합하는데 사용할 두번째 함수를 전달한다. 완성된 호출 형태


int result = words.reduce(0, (total, word) -> total + word.length(), (total1, total2) -> total1 + total2 );


실전에는 reduce 메서드를 많이 사용하지 않을 것이다.

보통 숫자 스트림에 매핑한 후 스트림의 합계, 최대값, 최소값 계산 메서드를 사용하는 것이 더 쉽다

words.mapToInt(String::length).sum()을 호출하는 방법으로 해결할수 있고, 

이렇게 하면 박싱 boxing이 일어나지 않기 때문에 더 단순하면서도 더 효율적이다.


결과 모으기


스트림 작업을 마칠 때 보통은 값으로 리듀스하기보다는 결과를 살펴보기 원하기 마련이다.

이때 요소들을 방문하는 데 사용할 수 있는 전통적인 반복자를 돌려주는 iterator 메서드를 호출할 수 있다.

다른방법으로 toArray를 호출해서 스트림 요소들의 배열을 얻을 수 있다.


실행시간 runtime에 제네릭 배열을 생성할 수 없기 때문에 Stream.toArray()는 Object[]를 리턴한다.

올바른 타입의 배열을 원하는 경우 다음과 같이 배열 생성자를 전달한다.


String[] result = words.toArray(String[]::new); //words.toArray()는 Object[] 타입을 리턴한다.


이제 HashSet에 결과들을 모으려 한다고 하자.

HashSet 객체는 스레드에 안전하지 않기 때문에, 컬렉션을 병렬화하면 요소들을 단일 HashSet에 직접 넣을 수 없다.

이와 같은 이유로 reduce를 사용할 수 없다.

각 부분은 자체적인 빈 해시 집합으로 작업을 시작해야 하는데, reduce는 항등값 하나만 전달하도록 허용한다.

따라서 reduce 대신 collect를 사용해한다. collect는 세가지 인자를 받는다.


1. 공급자 supplier: 대상 객체의 새로운 인스턴스를 만든다 (예-HashSet 생성자) 

2. 누산자 accumulator: 요소를 대상에 추가한다 (예-add 메서드)

3. 결합자 combiner: 두 객체를 하나로 병합한다 (예- addAll 메서드)


대상 객체가 컬렉션일 필요는 없다. StringBuilder나 카운트와 합계를 관리하는 객체라면 대상이 될 수 있다.


예) 해시 집합을 대상으로 collect 메서드가 동작하는 방법

HashSet<String> result = stream.collect(HashSet::new, HashSet::add, HashSet::addAll);


실전에서는 이들 세 함수를 제공하는 편리한 Collector 인터페이스와 공통 컬렉터용 팩토리 메서드를 제공하는  Collectors 클래스가 있으므로 이와 같이 일일이 지정할 필요가 없다.

스트림을 리스트나 집합으로 모으려면 단순히 다음과 같이 호출할 수 있다.


List<String> result = stream.collect(Collector.toList());


Set 버전

Set<String> result = stream.collect(Collectors.toSet());


어떤 집합 종류를 얻을지 제어하고 싶으면

TreeSet<String> result = stream.collect(Collectors.toCollection(TreeSet::new));


스트림에 있는 모든 문자열을 서로 연결해서 모으려고 한다고 하자.

String result = stream.collect(Collectors.joining());


요소간에 구분자가 필요하면 해당 구분자를 joining 메서드에 전달한다

String result = stream.collect(Collectors.joining(", "));


스트림이 문자열 외의 객체를 포함하는 경우, 먼저 해당 객체들을 문자열로 변환해야 한다.

String result =  stream.map(Object::toString).collect(Collectors.joining(", "));


스트림 결과를 합계, 평균, 최대값, 최소값으로 리듀스하려는 경우 summarizing(Int|Long|Double) 메서드중 하나를 사용한다.

이들 메서드는 스트림 객체를 숫자로 맵핑하는 함수를 받고 합계, 평균, 최대값, 최소값을 얻는 메서드를 제공하는 (Int|Long|Double) SummaryStatistics 타입 결과를 돌려준다.


IntSummaryStatistics summary = words.collect(Collectors.summarizingInt(String::length));


double averageWordLength = summary.getAverage();

double maxWordLength = summary.getMax();


단순히 값을 출력하거나 데이터베이스에 저장하고 싶을 수도 있다.

이때는 forEach 메서드를 사용할 수 있다.


stream.forEach(System.out::println);


forEach 메서드는 전달하는 함수가 각 요소에 적용된다.

함수를 병렬 스트림에서 동시에 실행할 수 있게 하는 일은 개발자의 몫이다.

병렬 스트림에서는 요소들을 임의 순서로 순회할 수 있다.

스트림 순서로 실행하고 싶으면 대신 forEachOrdered 메서드를 호출한다.

물론 이 경우 병렬성이 주는 대부분 또는 모든 이점을 포기해야 할수도 있다.


forEach와 forEachOrdered 메서드는 최종 연산이다.

이들 메서드를 호출한 후에는 스트림을 사용할 수 없다.

스트림을 계속 사용하고 싶으면 대신 peek를 사용해야 된다.


맵으로 모으기


Stream<Person>의 요소들을 맵으로 모아서 추후 ID로 사람을 조회할 수 있게 한다고 하자.

Collectors.toMap 메서드는 각각 맵의 키와 값을 생성하는 두 함수 인자를 받는다.

예)

Map<Integer, String> idToName = people.collect(Collectors.toMap(Person::getId, Person::getName));

Map<Integer, String> idToName = persons.stream().collect(Collectors.toMap(Person::getId, Person::getName));


값이 실제 요소여야 하는 일반적인 경우에서도 두번째 함수로 Function.identity()를 사용한다.

Map<Integer, Person> idToPerson = people.collect(Collectors.toMap(Person::getId, Function.identity()));

Map<Integer, Person> idToPerson = persons.stream().collect(Collectors.toMap(Person::getId, Function.identity()));


키가 같은 요소가 두개 이상이면 컬렉터는 IllegalStateException을 던진다.

이 동작은 기존 값과 새 값을 받아서 키에 해당하는 값을 결정하는 세 번째 함수 인자를 제공하는 방법으로 재정의할 수 있다.

여기서 세번째 인자로 제공하는 함수는 기존 값, 새값 또는 두 값의 조합을 리턴할 수 있다.


여기서는 사용 가능한 로케일locale에 있는 각 언어를 포함하는 맵을 생성한다.

이 맵에서 키는 디폴트 로케일에서 언어 이름(예-"German"), 값은 지역화된 이름("Deutsch")이다.


Stream<Locale> locales = Stream.of(Locale.getAvailableLocales());

Map<String, String> languageNames = locales.collect(

Collectors.toMap(

l -> l.getDisplayLanguage(),

l -> l.getDisplayLanguage(l),

(existingValue, newValue) -> existingValue)

);


이 예제에서는 같은 언어가 두 번 나타날 수 있다는 점(예- 독일과 스위스의 독일어)는 고려하지 않고 그처 첫번째 항목만 유지한다.


하지만 특정 국가에서 사용하는 모든 언어를 알고 싶다고 하자.

이 경우 Map<String, Set<String>>이 필요하다.

예) "Switzerland"에 해당하는 집합은[French, German, Italian]이다.

먼저 각 언어에 해당하는 싱글톤 singleton 집합을 마련한다.

그런 다음 주어진 국가에서 새로운 언어를 발견할 때마다 기존 집합과 새 집합의 합집합 union을 만든다.


Map<String, Set<String>> countryLanguageSets = locales.collect(

Collectors.toMap(

l -> l.getDisplayCountry()

l -> Collectons.singleton(l.getDisplayLanguage()),

(a,b) ->{

Set<String> r = new HashSet<>(a);

r.addAll(b);

return r;

}

)

);


TreeMap을 원할 경우 네번째 인자로 TreeMap 생성자를  전달한다.

이때 반드시 병합함수 merge function을 제공해야 한다.


Map<Integer, Person> idToPerson = people.collect(

Collectors.toMap(

Person::getId,

Function.identity(),

(existingValue, newValue) -> { Throw new  IllegalStateException(); }

TreeMap::new

)

)


toMap 메서드의 각 형태에 대응해 병행 맵을 리턴하는 toConcurrentMap 메서드가 있다.

병렬 컬렉션 처리에서는 병행 맵 하나를 사용한다.

병렬 스트림과 함께 사용하면 공유 맵하나가 여러 맵을 병합하는 방법보다 효율적이다.

물론 이 경우 정렬은 포기해야 한다.


그룹핑과 파티셔닝

groupingBy 메서드는 그룹 작업을 직접 지원한다.


Map<String, List<Locale>> countryToLocales = locales.collect(Collectors.groupingBy(Locale::getCountry));

Locale::getCountry는 그룹핑의 분류합수 classifier function이다.


지정한 국가 코드에 해당하는 모든 로케일을 조회

List<Locale> swissLocales = countryToLocales.get("CH"); //[fr_CH, de_CH, it_CH]


로케일관련

각 로케일은 언어코드(영어인경우  en)와 국가코드(미국인경우 US)를 포함한다.

로케일 en_US는 미국에서 사요하는 영어를 말하며, en_IE는 아일랜드에서 사용하는 영어를 말한다.

몇몇 국가에는 여러 로케일이 있다.

ga_IE는 아일랜드에서 사용하는 게일어

fr_CH, de_CH, it_CH 스위스는 세가지 로케일이 있다.


분류함수가 프레디케이드 함수(즉 boolean을 리턴하는 함수)인경우, 스트림 요소가 리스트 두개(각각 함수에서 true와  false를 리턴하는 경우에 해당)로 분할된다.

이 경우에는 groupingBy 대신 partitioningBy를 사용하면 훨씬 효율적이다.

예) 모든 로케일을 영어를 사용하는 경우와 그 외의 경우로 분리

Map<Boolean, List<Locale>> englishAndOtherLocales = locales.collect( Collectors.partitioningBy(l -> l.getLanguage().equals("en")));

List<Locale> englishLocales = englishAndOtherLocales.get(true);


groupingByConcurrent 메서드를 호출하면 병행 맵을 얻으며, 이를 병렬 스트림에서 사용하면 동시에 내용이 채워진다.

전체적으로 볼 때 이 메서드는 toConcurrentMap 메서드에 해당한다.


groupingBy 메서드는 값이 리스트인 맵을 돌려준다.

이들 리스트를 특정 방식으로 처리하려면 '다운스트림 컬렉터 downstream collector'를 제공한다.

예) 리스트 대신 집합을 원하는 경우 Collectors.toSet 컬렉터를 사용

Map<String, Set<Locale>> countryToLocaleSet = locales.collect(groupingBy(Locale::getCountry, toSet()));

import static java.util.stream.Collectors.*; // 정적 임포트 해야함


그룹으로 묶인 요소들의 다운스트림 처리용으로 몇가지 다른 컬렉터가 있다.

예) 각 국가의 로케일 개수

Map<String, Long> countryToLocaleCounts = locales.collect(groupingBy(Locale::getCountry, counting()));

counting은 모인 요소들의 개수를 센다.


예) 도시로 구성된 스트림에서 주별 인구의 합계

Map<String, Integer> stateToCityPopulation = cities.collect(groupingBy(City::getState, summingInt(City::getPopulation)));


예)  주별로 가장 큰 도시를 구한다.

Map<String, City> stateToLargestCity = cities.collection(groupingBy(City::getState, maxBy(Comparator.comparing(City::getPopulation))));


mapping은 함수를 다운스트림 결과에 적용하여, 이 결과를 처리하는데 필요한 또 다른 컬렉터를 요구한다.

예) 도시를 주별로 묶는다. 각주에서 도시들의 이름을 얻고 최대 길이로 리듀스한다.


Map<String, Optional<String>> stateToLongestCityName = cities.stream().collect(groupingBy(City::getState, mapping(City::getName, maxBy(Comparator.comparing(String::length)))));


예) 각 국가에서 사용하는 모든 언어의 집합을 모으려면?

Map<String, Set<String>> countryToLanguagees = locales.collect(

groupingBy(

l->l.getDisplayCountry(),

mapping(l-> l.getDisplayLanguage(),

toSet())));


그룹핑이나 맵핑 함수가 int, long 또는 double 타입을 리턴한다면 요소들을 요약 통계 객체안으로 모을 수 있다.

예) 각 그룹의요약 통계 객체로부터 함수 값들의 합계, 카운트, 평균, 최소값, 최대값


Map<String, IntSummaryStatistics> stateToCityPopulationSummary = cities.stream().collect(

groupingBy(City::getState,

summarizingInt(City::getPopulation))

);



reducing 메서드들은 다운 스트림 요소들에 범용 리덕션을 적용한다.

세가지 메서드 형태 reducing(binaryOperator), reducing(identity, binaryOperator), reducing(identity, mapper, binaryOperator)가 있다.

첫번재 형태에서는 항등값이 null이다.(항등 파라미터가 없으면 Opitonal 결과를 돌려주는 Stream::reduce의 형태와는 다르다)

세번째 형태에서는 mapper 함수가 적용되고 이 함수의 값이 리듀스된다.


예) 각 주에 있는 도시의 이름을 콤마 분리 문자열로 얻는 예제

Map<String, String> stateToCityNames = cities.stream().collect(

groupingBy(City::getState, 

reducing("", City::getName, 

(s, t)-> s.length() == 0 ? t : s + ", " + t)));



Stream.reduce와 마찬가지로 Collectors.reducing은 거의 사용할 필요가 없다.

Map<String, String> stateToCityNames = cities.stream().collect(

groupingBy(City::getState,

mapping(City::getName,

joining(", "))));


다운스트림 컬렉터는 아주 난해한 표현식을 야기할 수 있다.

따라서 '다운 스트림'맵 값들을 처리하기 위해서 반드시 groupingBy 또는 partitioningBy와 연계해서 사용해야 한다.

그렇지 않으면 단순히 map, reduce, count, max, min 같은 메서드를 스트림에 직접 적용한다.

반응형