Hello!
In my StreamEx library I created a "pairing" collector which does
similar job, but allows user to decide how to combine the results of
two collectors. This adds more flexibility. The signature is like this:
public static <T, A1, A2, R1, R2, R> Collector<T, ?, R> pairing(
Collector<? super T, A1, R1> c1,
Collector<? super T, A2, R2> c2,
BiFunction<? super R1, ? super R2, ? extends R> finisher)
Having such collector, The proposed `toBoth(c1, c2)` can be
implemented as simple as `pairing(c1, c2, Map::entry)`. OTOH if
somebody wants to use their own Pair class, it would be `pairing(c1,
c2, Pair::new)`.
Sometimes you don't need a pair, but can create compound result object
right here. E.g.:
Collector<BigDecimal, ?, BigDecimal> summing =
Collectors.reducing(BigDecimal.ZERO, BigDecimal::add);
Collector<BigDecimal, ?, BigDecimal> averaging =
pairing(summing, Collectors.counting(), (sum, count) ->
sum.divide(BigDecimal.valueOf(count),
RoundingMode.HALF_EVEN));
So locking to Map.Entry is entirely unnecessary.
With best regards,
Tagir Valeev.
On Thu, Jun 14, 2018 at 6:11 AM Brian Goetz <brian.go...@oracle.com
<mailto:brian.go...@oracle.com>> wrote:
I really like how BiCollector can be private, so all we'd have to
expose
is toBoth(), and the arguments of toBoth() are just ordinary
collectors. So no new types or abstractions; just a multiplexing.
The use of Map.Entry as a pair surrogate is unfortunate -- its
definitely not an Entry -- though I understand why you did this.
I'm not
sure if this is fatal or not for a JDK method, but it's pretty bad*.
(You could generalize to n-ary, returning a List or array (you can
take
a varargs of Collector), at the loss of sharp types for the results.)
*Yes, I'm sure one can find precedent of this being done; this has no
effect on whether it's bad.
On 6/11/2018 8:39 AM, Peter Levart wrote:
> Hi,
>
> Have you ever wanted to perform a collection of the same Stream
into
> two different targets using two Collectors? Say you wanted to
collect
> Map.Entry elements into two parallel lists, each of them containing
> keys and values respectively. Or you wanted to collect elements
into
> groups by some key, but also count them at the same time? Currently
> this is not possible to do with a single Stream. You have to create
> two identical streams, so you end up passing Supplier<Stream> to
other
> methods instead of bare Stream.
>
> I created a little utility Collector implementation that serves the
> purpose quite well:
>
> /**
> * A {@link Collector} implementation taking two delegate
Collector(s)
> and producing result composed
> * of two results produced by delegating collectors, wrapped in
{@link
> Map.Entry} object.
> *
> * @param <T> the type of elements collected
> * @param <K> the type of 1st delegate collector collected result
> * @param <V> tye type of 2nd delegate collector collected result
> */
> public class BiCollector<T, K, V> implements Collector<T,
> Map.Entry<Object, Object>, Map.Entry<K, V>> {
> private final Collector<T, Object, K> keyCollector;
> private final Collector<T, Object, V> valCollector;
>
> @SuppressWarnings("unchecked")
> public BiCollector(Collector<T, ?, K> keyCollector,
Collector<T,
> ?, V> valCollector) {
> this.keyCollector = (Collector)
> Objects.requireNonNull(keyCollector);
> this.valCollector = (Collector)
> Objects.requireNonNull(valCollector);
> }
>
> @Override
> public Supplier<Map.Entry<Object, Object>> supplier() {
> Supplier<Object> keySupplier = keyCollector.supplier();
> Supplier<Object> valSupplier = valCollector.supplier();
> return () -> new
> AbstractMap.SimpleImmutableEntry<>(keySupplier.get(),
valSupplier.get());
> }
>
> @Override
> public BiConsumer<Map.Entry<Object, Object>, T> accumulator() {
> BiConsumer<Object, T> keyAccumulator =
> keyCollector.accumulator();
> BiConsumer<Object, T> valAccumulator =
> valCollector.accumulator();
> return (accumulation, t) -> {
> keyAccumulator.accept(accumulation.getKey(), t);
> valAccumulator.accept(accumulation.getValue(), t);
> };
> }
>
> @Override
> public BinaryOperator<Map.Entry<Object, Object>> combiner() {
> BinaryOperator<Object> keyCombiner =
keyCollector.combiner();
> BinaryOperator<Object> valCombiner =
valCollector.combiner();
> return (accumulation1, accumulation2) -> new
> AbstractMap.SimpleImmutableEntry<>(
> keyCombiner.apply(accumulation1.getKey(),
> accumulation2.getKey()),
> valCombiner.apply(accumulation1.getValue(),
> accumulation2.getValue())
> );
> }
>
> @Override
> public Function<Map.Entry<Object, Object>, Map.Entry<K, V>>
> finisher() {
> Function<Object, K> keyFinisher = keyCollector.finisher();
> Function<Object, V> valFinisher = valCollector.finisher();
> return accumulation -> new
AbstractMap.SimpleImmutableEntry<>(
> keyFinisher.apply(accumulation.getKey()),
> valFinisher.apply(accumulation.getValue())
> );
> }
>
> @Override
> public Set<Characteristics> characteristics() {
> EnumSet<Characteristics> intersection =
> EnumSet.copyOf(keyCollector.characteristics());
> intersection.retainAll(valCollector.characteristics());
> return intersection;
> }
> }
>
>
> Do you think this class is general enough to be part of standard
> Collectors repertoire?
>
> For example, accessed via factory method
Collectors.toBoth(Collector
> coll1, Collector coll2), bi-collection could then be coded
simply as:
>
> Map<String, Integer> map = ...
>
> Map.Entry<List<String>, List<Integer>> keys_values =
> map.entrySet()
> .stream()
> .collect(
> toBoth(
> mapping(Map.Entry::getKey, toList()),
> mapping(Map.Entry::getValue, toList())
> )
> );
>
>
> Map.Entry<Map<Integer, Long>, Long> histogram_count =
> ThreadLocalRandom
> .current()
> .ints(100, 0, 10)
> .boxed()
> .collect(
> toBoth(
> groupingBy(Function.identity(), counting()),
> counting()
> )
> );
>
>
> Regards, Peter
>