BiCollector

Tagir Valeev amaembo at gmail.com
Thu Jun 14 04:56:25 UTC 2018


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.goetz at 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
> >
>
>


More information about the core-libs-dev mailing list