해당 내용은 Java8 In Action 책을 요약 및 정리한 내용입니다.
좀 더 자세히 알고 싶으신 분들은 책을 사서 읽어보심을 추천드립니다.!
6.4 분할
Entity
Dish.java
package Part2.Chapter6.Chapter6_6_4.entity;
public class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private Type type;
public Dish(String name, boolean vegetarian, int calories, Type type) {
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
public String getName() {
return this.name;
}
public int getCalories() {
return this.calories;
}
public Type getType() {
return this.type;
}
public boolean isVegetarian() {
return vegetarian;
}
@Override
public String toString() {
return this.name;
}
public enum Type {
MEAT, FISH, OTHER
}
}
Main
package Part2.Chapter6.Chapter6_6_4;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import Part2.Chapter6.Chapter6_6_4.entity.Dish;
/*
* 6.4 분할
*
* 분할은 "분할 함수(partitioning function)"라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능이다.
* 분할 함수는 불린을 반환하므로 맵의 키 형식은 Boolean이다.
* 결과적으로 그룹화 맵은 최대(참 아니면 거짓의 값을 갖는) 두 개의 그룹으로 분류 된다.
*/
public class Main_6_4 {
public static void main(String[] args) {
List<Dish> menu = Arrays.asList(
new Dish("pork", false, 800, Dish.Type.MEAT)
, new Dish("beef", false, 700, Dish.Type.MEAT)
, new Dish("chicken", false, 400, Dish.Type.MEAT)
, new Dish("french fries", true, 530, Dish.Type.OTHER)
, new Dish("rice", true, 350, Dish.Type.OTHER)
, new Dish("season fruit", true, 120, Dish.Type.OTHER)
, new Dish("pizza", true, 550, Dish.Type.OTHER)
, new Dish("prawns", false, 300, Dish.Type.FISH)
, new Dish("salmon", false, 450, Dish.Type.FISH));
/*
* 아래 코드는 모든 요리를 채식 요리와 채식이 아닌 요리로 분류한 코드이다.
*/
Map<Boolean, List<Dish>> partitionedMenu = menu.stream()
.collect(Collectors.partitioningBy(Dish::isVegetarian));
System.out.println("6.4 분할 : " +partitionedMenu);
System.out.println("6.4 분할(채식 요리): " + partitionedMenu.get(true));
System.out.println("6.4 분할(채식이 아닌 요리): " + partitionedMenu.get(false));
Predicate<Dish> isVegetarian = Dish::isVegetarian;
System.out.println("6.4 분할(filter 메서드 사용 - 채식 요리) : " + menu.stream()
.filter(isVegetarian)
.collect(Collectors.toList()) );
System.out.println("6.4 분할(filter 메서드 사용 - 채식이 아닌 요리) : " + menu.stream()
.filter(isVegetarian.negate())
.collect(Collectors.toList()) );
/*
* 6.4.1 분할의 장점
*
* 분할 함수가 반환하는 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다는 것이 분할의 장점이다.
* 다음 코드에서는 컬렉터를 두 번째 인수로 전달할 수 있는 오버로드된 버전의 partitioningBy 메서드도 있다.
*/
System.out.println("6.4.1 분할의 장점 : " + menu.stream()
.collect(Collectors.partitioningBy(Dish::isVegetarian
, Collectors.groupingBy(Dish::getType)) ));
/*
* 채식 요리와 채식이 아닌 요리 각각 그룹에서 칼로리가 높은 요리.
*/
System.out.println("6.4.1 분할의 장점 : " + menu.stream()
.collect(Collectors.partitioningBy(Dish::isVegetarian
, Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)), Optional::get)) ));
/*
* 6.4.2 숫자를 소수와 비소수로 분할하기
*
* 정수 n을 인수로 받아서 2에서 n까지의 자연수를 소수(prime)와 비소수(nonprime)로 나누는 코드를 구현하자.
*/
System.out.println("6.4.2 숫자를 소수와 비소수로 분할하기 : " + IntStream.rangeClosed(2, 100).boxed()
.collect(Collectors.partitioningBy(candidate -> isPrime(candidate)) ));
/*
* Collectors 클래스의 정적 팩토리 메서드
*
* 팩토리 메서드 : toList
* 반환 형식 : List<T>
* 사용 예제 : 스트림의 모든 항목을 리스트로 수집.
* 활용 예 : List<Dish> dishes = menuStream.collect(Collectors.toList());
*
* 팩토리 메서드 : toSet
* 반환 형식 : Set<T>
* 사용 예제 : 스트림의 모든 항목을 중복이 없는 집합으로 수집.
* 활용 예 : Set<Dish> dishes = menuStream.collect(Collectors.toSet());
*
* 팩토리 메서드 : toCollection
* 반환 형식 : Collection<T>
* 사용 예제 : 스트림의 모든 항목을 공급자가 제공하는 컬렉션으로 수집.
* 활용 예 : Collection<Dish> dishes = menuStream.collect(Collectors.toCollection(), ArrayList::new);
*
* 팩토리 메서드 : counting
* 반환 형식 : Long
* 사용 예제 : 스트림의 항목 수 계산.
* 활용 예 : Long howManyDishes = menuStream.collect(Collectors.counting());
*
* 팩토리 메서드 : summingInt
* 반환 형식 : Integer
* 사용 예제 : 스트림의 항목에서 정수 프로퍼티값을 더함.
* 활용 예 : int totalCalories = menuStream.collect(Collectors.sumingInt(Dish::getCalories));
*
* 팩토리 메서드 : averagingInt
* 반환 형식 : Double
* 사용 예제 : 스트림 항목의 정수 프로퍼티의 평균값 계산.
* 활용 예 : double avgCalories = menuStream.collect(Collectors.averagingInt(Dish:getCalories));
*
* 팩토리 메서드 : summarizingInt
* 반환 형식 : IntSummaryStatistics
* 사용 예제 : 스트림 내의 항목의 최대값, 최소값, 합계 평균 등의 정수 정보 통계를 수집.
* 활용 예 : IntSummaryStatistics menuStatistics = menuStream.collect(Collectors.summarizingInt(Dish::getCalories));
*
* 팩토리 메서드 : joining
* 반환 형식 : String
* 사용 예제 : 스트림의 각 항목에 toString 메서드를 호출한 결과 문자열을 연결.
* 활용 예 : String shortMenu = menuStream.map(Dish::getName).collect(Collectors.joining(", "));
*
* 팩토리 메서드 : maxBy
* 반환 형식 : Optional<T>
* 사용 예제 : 주어진 비교자를 이용해서 스트림의 최대값 요소를 Optional로 감싼 값을 반환. 스트림에 요소가 없을 때는 Optional.empty()을 반환.
* 활용 예 : Optional<Dish> fattest = menuStream.collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)));
*
* 팩토리 메서드 : minBy
* 반환 형식 : Optional<T>
* 사용 예제 : 주어진 비교자를 이용해서 스트림의 최소값 요소를 Optional로 감싼 값을 반환. 스트림에 요소가 없을 때는 Optional.empty()을 반환.
* 활용 예 : Optional<Dish> fattest = menuStream.collect(Collectors.minBy(Comparator.comparingInt(Dish::getCalories)));
*
* 팩토리 메서드 : reducing
* 반환 형식 : 리듀싱 연산에서 형식을 결정
* 사용 예제 : 누적자를 초기값으로 설정한 다음에 BinaryOperator로 스트림의 각 요소를 반복적으로 누적자와 합쳐 스트림을 하나의 값으로 리듀싱.
* 활용 예 : int totalCalories = menuStream.collect(Collectors.reducing(0, Dish::getCalories, Integer::sum));
*
* 팩토리 메서드 : collectingAndThen
* 반환 형식 : 변환 함수가 형식을 결정
* 사용 예제 : 다른 컬렉터를 감싸고 그 결과에 변환 함수를 적용
* 활용 예 : int howManyDishes = menuStream.collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
*
* 팩토리 메서드 : groupingBy
* 반환 형식 : Map<K, List<T>>
* 사용 예제 : 하나의 프로퍼티값을 기준으로 스트림의 항목을 그룹화하며 기준 프로퍼티 값을 결과 맵의 키로 사용.
* 활용 예 : Map<Dish.Type, List<Dish>> dishesByType = menuStream.collect(Collectors.groupingBy(Dish::getType));
*
* 팩토리 메서드 : partitioningBy
* 반환 형식 : Map<Boolean, List<T>>
* 사용 예제 : 프레디케이트를 스트림의 각 항목에 적용한 결과로 항목을 분할.
* 활용 예 : Map<Boolean, List<Dish>> vegetarianDishes = menuStream.collect(Collectors.partitioningBy(Dish::isVegetarian));
*/
}
public static boolean isPrime(int candidate) {
int candidateRoot = (int) Math.sqrt((double) candidate);
return IntStream.rangeClosed(2, candidateRoot)
.noneMatch(i -> candidate % i == 0);
}
}
6.5 Collector 인터페이스
Entity
Dish.java
package Part2.Chapter6.Chapter6_6_5.entity;
public class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private Type type;
public Dish(String name, boolean vegetarian, int calories, Type type) {
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
public String getName() {
return this.name;
}
public int getCalories() {
return this.calories;
}
public Type getType() {
return this.type;
}
public boolean isVegetarian() {
return vegetarian;
}
@Override
public String toString() {
return this.name;
}
public enum Type {
MEAT, FISH, OTHER
}
}
Main
package Part2.Chapter6.Chapter6_6_5;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import Part2.Chapter6.Chapter6_6_5.entity.Dish;
/*
* 6.5 Collector 인터페이스
*
* Collector 인터페이스는 리듀싱 연산(즉, 컬렉터)을 어떻게 구현할지 제공하는 메서드 집합으로 구성된다.
*
* Collector 인터페이스
* public interface Collector<T, A, R> {
* Supplier<A> supplier();
* BiConsumer<A, T> accumulator();
* Function<A, R> finisher();
* BinarayOperator<A> cambiner();
* Set<Characteristics> characteristics();
* }
*
* 위 코드는 다음처럼 설명할 수 있다.
* - T는 수집될 스트림 항목의 제네릭 형식이다.
* - A는 누적자, 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식이다.
* - R은 수집 연산 결과 객체의 형식이다.
* (항상 그런 것은 아니지만 대개 컬렉터 형식)
*/
public class Main_6_5 {
public static void main(String[] args) {
List<Dish> menu = Arrays.asList(
new Dish("pork", false, 800, Dish.Type.MEAT)
, new Dish("beef", false, 700, Dish.Type.MEAT)
, new Dish("chicken", false, 400, Dish.Type.MEAT)
, new Dish("french fries", true, 530, Dish.Type.OTHER)
, new Dish("rice", true, 350, Dish.Type.OTHER)
, new Dish("season fruit", true, 120, Dish.Type.OTHER)
, new Dish("pizza", true, 550, Dish.Type.OTHER)
, new Dish("prawns", false, 300, Dish.Type.FISH)
, new Dish("salmon", false, 450, Dish.Type.FISH));
System.out.println("Collectors.toList : " + menu.stream().collect(Collectors.toList()) );
System.out.println("ToListCollector : " + menu.stream().collect(new ToListCollector<Dish>()) );
/*
* 컬렉터 구현을 만들지 않고도 커스텀 수집 수행하기
*
* IDENTITY_FINISH 수집 연산에서는 Collector 인터페이스를 완전히 새로 구현하지 않고도 같은 결과를 얻을 수 있다.
* Stream은 세 함수(supplier, accumulator, combiner)를 인수로 받는 collect 메서드를 오버로드하며 각각의 메서드는
* Collector 인터페이스의 메서드가 반환하는 함수와 같은 기능을 한다.
*
* 아래 코드가 좀 더 간결하고 축약되어 있지만 가독성은 떨어진다.
* 적절한 클래스로 커스텀 컬렉터를 구현하는 편이 중복을 피하고 재사용성을 높이는데 도움이 된다.
* 또한 Characteristics를 전달할 수 없다. 즉, 두 번째 collect 메서드는 IDENTITY_FINISH와 CONCURRENT지만
* UNORDERED는 아닌 컬렉터로만 동작한다.
*/
System.out.println("인수 사용 : " + menu.stream().collect(ArrayList::new, List::add, List::addAll));
}
}
/*
* 6.5.1 Collector 인터페이스의 메서드 살펴보기
*
* 네 개의 메서드는 collect 메서드에서 실행하는 함수를 반환하는 반면, 다섯 번째 메서드 characteristics는
* collect 메서드가 어떤 최적화(병렬화 같은)를 이용해서 리듀싱 연산을 수행할 것인지 결정하도록 돕는
* 힌트 특성 집합을 제공한다.
*
* 실제로 collect가 동작하기 전에 다른 중간 연산과 파이프라인을 구성할 수 있게 해주는 게으른 특성 그리고
* 병렬실행 등도 고려해야 하므로 스트림 리듀싱 기능 구현은 생각보다 복잡하다.
*/
class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
/*
* supplier 메서드 : 새로운 결과 컨테이너 만들기
*/
@Override
public Supplier<List<T>> supplier() {
/*
* supplier 메서드는 빈 결과로 이루어진 Supplier를 반환해야 한다.
* 즉, supplier는 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수다.
*
* 해당 클래스처럼 누적자를 반환하는 컬렉터에서는 빈 누적자가 비어있는 스트림의 수집 과정의 결과가 될 수 있다.
*/
return () -> new ArrayList<T>();
/*
* 아래와 같이 하면 더 간결해진다.
*/
// return ArrayList::new;
}
/*
* accumulator 메서드 : 결과 컨테이너에 요소 추가하기
*/
@Override
public BiConsumer<List<T>, T> accumulator() {
/*
* accumulator 메서드는 리듀싱 연산을 수행하는 함수를 반환한다.
* 스트림에서 n번째 요소를 탐색할 때 두 인수, 즉 누적자(스트림의 첫 n-1개 항목을 수집한 상태)와 n번째 요소를 함수에 적용한다.
* 함수의 반환값은 void, 즉 요소를 탐색하면서 적용하는 함수에 의해 누적자 내부 상태가 바뀌므로 누적자가 어떤 값일지 단정할 수 없다.
*
* 해당 클래스에서 accumulator가 반환하는 함수는 이미 탐색한 항목을 포함하는 리스트에 현재 항목을 추가하는 연산을 수행한다.
*/
return (List<T> list, T item) -> list.add(item);
/*
* 아래와 같이 하면 더 간결해진다.
*/
// return List::add;
}
/*
* finisher 메서드 : 최종 변환값을 결과 컨테이너로 적용하기
*/
@Override
public Function<List<T>, List<T>> finisher() {
/*
* finisher 메서드는 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끌낼 때 호출할 함수를 반환해야 한다.
* 때로는 해당 클래스처럼 누적자 객체가 이미 최종 결과인 상황도 있다.
*
* 이런 때는 변환 과정이 필요하지 않으므로 finisher 메서드는 항등 함수를 반환한다.
*
* supplier, accumulator, finisher의 세 가지 메서드로도 순차적 스트림 리듀싱 기능을 수행할 수 있다.
*/
return (list) -> list;
/*
* 아래와 같이 하면 더 간결해진다.
*/
//return Function.identity();
}
/*
* combiner 메서드 : 두 결과 컨테이너 병합
*/
@Override
public BinaryOperator<List<T>> combiner() {
/*
* combiner는 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의한다.
* toList의 combiner는 스트림의 두 번째 서브파트에서 수집한 항목 리스트를 첫 번째 서브파트 결과 리스트 뒤에 추가하면 되기 때문에
* 비교적 쉽게 구현할 수 있다.
*/
return (list1, list2) -> {
list1.addAll(list2);
return list1;
};
}
/*
* Characteristics 메서드는 컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환한다.
*/
@Override
public Set<Characteristics> characteristics() {
/*
* Characteristics는 스트림을 병렬로 리듀스할 것인지 그리고 병렬로 리듀스한다면 어떤 최적화를 선택해야 할지 힌트를 제공한다.
*
* UNORDERED
* 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않는다.
*
* CONCURRENT
* 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있으며 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다.
* 컬렉터의 플래그에 UNORDERED를 함께 설정하지 않았다면 데이터 소스가 정렬되어 있지 않은(집합처럼 요소의 순서에 무의미한)상황에서만
* 병렬 리듀싱을 수행할 수 있다.
*
* IDENTITY_FINISH
* finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 이를 생략할 수 있다.
* 따라서 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용할 수 있다.
* 또한 누적자 A를 결과 R로 안전하게 형변환할 수 있다.
*
* 해당 클래스는 스트림의 요소를 누적하는데 사용한 리스트가 최종 결과 형식이므로 추가 변환이 필요없다.
* 그러므로 IDENTITY_FINISH다. 하지만 리스트의 순서는 상관이 없으므로 UNORDERED다. 그리고 마지막으로 CONCURRENT다.
* 하지만 이미 설명했듯이 요소의 순서가 무의미한 데이터 소스여야 병렬로 실행할 수 있다.
*/
return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT));
}
}
6.6 커스텀 컬렉터를 구현해서 성능 개선하기
Main
package Part2.Chapter6.Chapter6_6_6;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/*
* 6.6 커스텀 컬렉터를 구현해서 성능 개선하기
*
* 커스텀 컬렉터로 n까지의 자연수를 소수와 비소수 분할 성능 개선.
*/
/*
* 1단계 : Collector 클래스 시그너처 정의
*
* 정수로 이루어진 스트림에서 누적자와 최종 결과의 형식이 Map<Boolean, List<Integer>>인 컬렉터를 구현한다.
* 즉, Map<Boolean, List<Integer>>는 참과 거짓을 키로 소수와 비소수를 구분 짓는다.
*/
class PrimeNumbersCollector implements Collector<Integer, Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> {
private <A> List<A> takeWhile(List<A> list, Predicate<A> p) {
int i = 0;
for(A item : list) {
// 리스트의 현재 요소가 프레디케이트를 만족하는지 검사한다.
if(!p.test(item)) {
// 프레디케이트를 만족하지 않으면 검사한 항목의 앞쪽에 위치한 서브 리스트를 반환한다.
return list.subList(0, i);
}
i++;
}
return list;
}
private boolean isPrime(List<Integer> primes, int candidate) {
int candidateRoot = (int) Math.sqrt((double) candidate);
return takeWhile(primes, i -> {
return i <= candidateRoot;
})
.stream().noneMatch(i -> {
return candidate % i == 0;
});
}
/*
* 2단계 : 리듀싱 연산 구현
*/
@Override
public Supplier<Map<Boolean, List<Integer>>> supplier() {
/*
* 누적자로 사용할 맵을 만들면서 true, false 키와 빈 리스트로 초기화 한다.
* 수집 과정에서 빈 리스트에 각각 소수와 비소수를 추가할 것이다.
*/
return () -> new HashMap<Boolean, List<Integer>>() {
private static final long serialVersionUID = 1L;
{
put(true, new ArrayList<Integer>());
put(false, new ArrayList<Integer>());
}
};
}
@Override
public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
/*
* 지금까지 발견한 소수 리스트(누적 맵의 true 키로 이들 값에 접근할 수 있다)와 소수 여부를 확인하고 싶은 candidate를
* 인수로 isPrime 메서드를 호출 했다.
* isPrime의 호출 결과로 소수 리스트 또는 비소수 리스트 중 알맞는 리스트로 candidate를 추가한다.
*/
return (Map<Boolean, List<Integer>> acc, Integer candidate) -> {
acc.get(isPrime(acc.get(true), candidate)).add(candidate);
};
}
/*
* 3단계 : 병렬 실행할 수 있는 컬렉터 만들기(가능하다면)
*
* 예제에서는 단순하게 두 번째 맵의 소수 리스트와 비소스 리스트의 모둔 수를 첫 번째 맵에 추가하는 연산이면 충분하다.
*/
@Override
public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
/*
* 참고로 알고리즘 자체가 순차적이어서 컬렉터를 실제 병렬로 사용할 순 없다.
* 따라서 combiner 메서드는 호출될 일이 없으므로 빈 구현으로 남겨두거나 UnsupportedOperationException을 던지도록 구현한다.
* 실제로 이 메서드는 사용할 일이 없지만 학습을 목적으로 구현한 것이다.
*/
return (Map<Boolean, List<Integer>> map1, Map<Boolean, List<Integer>> map2) -> {
map1.get(true).addAll(map2.get(true));
map1.get(false).addAll(map2.get(false));
return map1;
};
}
/*
* 4단계 : finisher 메서드와 컬렉터의 characteristics 메서드
*/
@Override
public Function<Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> finisher() {
/*
* accumulator의 형식은 컬렉터 결과 형식과 같으므로 변환 과정이 필요없다. 따라서 항등 함수 identity를 반환한다.
*/
return Function.identity();
}
@Override
public Set<Characteristics> characteristics() {
/*
* 해당 커스텀 컬렉터는 CONCURRENT도 아니고 UNORDERED도 아니지만 IDENTITY_FINISH이므로 아래 처럼 구현할 수 있다.
*/
return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH) );
}
}
public class Main_6_6 {
public static Map<Boolean, List<Integer>> partitionPrimes(int n) {
return IntStream.rangeClosed(2, n).boxed()
.collect(Collectors.partitioningBy(candidate -> {
int candidateRoot = (int) Math.sqrt((double) candidate);
return IntStream.rangeClosed(2, candidateRoot).noneMatch(i -> candidate % i == 0);
}));
}
public static Map<Boolean, List<Integer>> partitionPrimesCustomCollector(int n) {
return IntStream.rangeClosed(2, n).boxed()
.collect(new PrimeNumbersCollector());
}
public static <A> List<A> takeWhile(List<A> list, Predicate<A> p) {
int i = 0;
for(A item : list) {
// 리스트의 현재 요소가 프레디케이트를 만족하는지 검사한다.
if(!p.test(item)) {
// 프레디케이트를 만족하지 않으면 검사한 항목의 앞쪽에 위치한 서브 리스트를 반환한다.
return list.subList(0, i);
}
i++;
}
return list;
}
public static boolean isPrime(List<Integer> primes, int candidate) {
int candidateRoot = (int) Math.sqrt((double) candidate);
return takeWhile(primes, i -> {
return i <= candidateRoot;
})
.stream().noneMatch(i -> {
return candidate % i == 0;
});
}
public static Map<Boolean, List<Integer>> partitionPrimesWithCustomCollector(int n) {
/*
* collect 메서드의 오버로드를 이용해서 핵심 로직을 구현하는 세 함수를 전달해서 같은 결과를 얻을 수 있다.
* 코드는 간결하지만 재사용성은 떨어진다.
*/
return IntStream.rangeClosed(2, n).boxed()
.collect(
() -> new HashMap<Boolean, List<Integer>>() {
private static final long serialVersionUID = 1L;
{
put(true, new ArrayList<Integer>());
put(false, new ArrayList<Integer>());
}
}
, (acc, candidate) -> {
acc.get(isPrime(acc.get(true), candidate)).add(candidate);
}
, (map1, map2) -> {
map1.get(true).addAll(map2.get(true));
map1.get(false).addAll(map2.get(false));
}
);
}
public static void main(String[] args) {
/*
* 6.6.2 컬렉터 성능 비교
*/
long fastest = Long.MAX_VALUE;
// 테스트를 10번 반복한다.
for(int i = 0; i < 10; i++) {
long start = System.nanoTime();
// 백만 개의 숫자를 소수와 비소수로 분할한다.
partitionPrimes(1000000);
// partitionPrimesCustomCollector(1000000);
// partitionPrimesWithCustomCollector(1000000);
// duration을 밀리초 단위로 측정한다.
long duration = (System.nanoTime() - start) / 1000000;
// 가장 빨리 실행되었는지 확인한다.
if(duration < fastest) {
fastest = duration;
}
System.out.println("Fastest execution done in " + fastest +" msecs");
}
}
}
요약
- collect는 스트림의 요소를 요약 결과로 누적하는 다양한 방법(컬렉터라 불리는)을 인수로 갖는 최종 연산이다.
- 스트림의 요소를 하나의 값으로 리듀스하고 요약하는 컬렉터뿐 아니라 최소값, 최대값, 평균값을 계산하는 컬렉터 등이 미리 정의되어 있다.
- 미리 정의된 컬렉터인 groupingBy로 스트림의 요소를 그룹화하거나, partitioningBy로 스트림의 요소를 분할할 수 있다.
- 컬렉터는 다수준의 그룹화, 분할, 리듀싱 연산에 적합하게 설계되어 있다.
- Collector 인터페이스에 정의된 메서드를 구현해서 커스텀 컬렉터를 개발할 수 있다.