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


Reply via email to