JAVA 8에서 추가된 stream API를 사용할 때, stream으로 넘어가기 이전의 collection에서 stream 내의 작업이 완료된 새 collection을 얻기를 원해서 하는 경우가 있다. reduce() 계열에 비해 오히려 이쪽이 더 자주 쓰이지 않을까? 나는 더 자주 쓴다.
이럴 때는 collect() 메서드가 유용하다.
기본 용법
용례는 다음과 같다.
1 | ArrayList<Object> result = objectList.stream().collect(ArrayList::new, ArrayList::add, ArrayList::addAll); |
이 예제의 인자들을 타입으로 보면 이렇다.
1 | objectList.stream().collect(Supplier<R> supplier, BiConsumer<R, ? super Object> accumulator, BiConsumer<R, R> combiner); |
여기에서 supplier란 어떤 collection의 생성자 레퍼런스이며, accumulator란 대상에 어떤 요소를 추가하는 function이고, combiner는 어떤 collection 2개를 병합하는 function임을 알 수 있다.
당연하지만 supplier, accumulator, combiner를 일일이 입력하는 것은 몹시 귀찮은 일이다. 이때 쓸 수 있는 Collectors.class
가 있다.
1 | List<Object> resultList = objectList.stream().collect(Collectors.toList()); |
그런데 이 List와 Set은 구체적으로 어떤 구현체로 되어있을까? 명시적으로 핸들링하고 싶다면 Collectors.toCollection(supplier)
를 사용할 수 있다.
1 | ArrayList<Object> resultArrayList = objectList.stream().collect(Collectors.toCollection(ArrayList::new)); |
joining
collect()
는 그 외에도 용도가 다양한 편인데, stream이 String 객체만을 대상으로 하고 있다면 Collectors.joining()
을 사용해 String을 결합할 수도 있다. 결합되는 String 사이에 무언가 다른 String을 끼워넣고 싶다면 .joining("String")
을 사용한다.
summary
그 외에도 int, double, long 객체를 가진 collection을 대상으로 단번에 sum, avg, max, min 값을 원할 때도 collect
가 유용할 수 있다.
1 | //IntSummaryStatistics, Collectors.summarizingInt 외에 DoubleSummaryStatistics 등이 있다. |
toMap
collect(Collectors.toMap())
의 경우는 조금 더 복잡하다.
1 | // key&value |
groupingBy와 partitioningBy
Collectors.toMap의 3번 째 parameter로 동일한 key를 가진 복수개의 요소를 처리할 수 있지만, collection에서 중복된 요소들을 모아 group을 만들기 위해서 map을 만드려고 하는 요구를 달성하기에는 좀 귀찮아진다. 때문에 이 작업을 위한 메서드가 별도로 있다. groupingBy
와 partitioningBy
가 그것이다.
1 | @Data |
이제 Person의 name으로 grouping 해보자. nameGroup은 Person::getName으로 분류된 그룹이므로 이름이 같인 Person을 묶어 List를 만들고, 그 이름을 Key로 하여 Map에 넣는다.
1 | Map<String, List<Person>> nameGroup = people.stream().collect(Collectors.groupingBy(Person::getName)); |
위의 Person::getName처럼 분류 목적으로 사용한 function을 classfier function이라고 한다. classfier function이 T t -> boolean
라면, 즉 Predicate<T>
라면 groupingBy 대신 partitioningBy를 쓸 수 있다.
1 | Map<Boolean, List<Person>> generationGroup = people.stream().collect(Collectors.partitioningBy(Person::isAdult)); |
Downstream collector
예제처럼 groupingBy나 partitioningBy를 그냥 쓰면 Map의 value는 List다. 개발을 하다보면 단순히 count만 필요하거나, Set인 것이 편할 때가 있다. downstream
을 던져넣어 List가 아닌 다른 value를 가진 Map을 얻을 수 있다.
1 | //타입으로 보면 이렇다. |
summingInt
, summarizingInt
, mapping
, maxBy
, minBy
, reducing
등 여러가지를 쓸 수 있으므로 적절히 사용하면 편리해지지만, depth가 너무 깊어지지 않게 주의하자. 하려고 하면 아주 복잡해질 수 있는 부분이다.
이외에도1
Collectors groupingBy(Function<? super T,? extends K> classifier, Supplier<M> mapFactory, Collector<? super T,A,D> downstream)
의 형태로 mapFactory를 넣을 수도 있는데.. 충분히 길어졌으므로 이 포스팅은 여기서 끊자.