Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 Stream API - Java 9 Collectors.flatMapping rewritten in Java 8

I got in touch with a new feature since java-9 called Collectors.flatMapping that takes place as a downstream of grouping or partitioning. Such as (example taken from here):

List<List<Integer>> list = Arrays.asList(
    Arrays.asList(1, 2, 3, 4, 5, 6), 
    Arrays.asList(7, 8, 9, 10));

Map<Integer, List<Integer>> map =list.stream()
    .collect(Collectors.groupingBy(
         Collection::size,
         Collectors.flatMapping(
             l -> l.stream().filter(i -> i % 2 == 0),
             Collectors.toList())));

{4=[8, 10], 6=[2, 4, 6]}

This is a fairly elegant way using just 3 collectors. I need to rewrite the collector in java-8 where is not yet supported. My attempt use 6 Collectors that is quite an extensive usage and I am not able to figure out a way using less of them:

Map<Integer, List<Integer>> map = list.stream()
    .collect(Collectors.groupingBy(
        Collection::size,
        Collectors.collectingAndThen(
            Collectors.mapping(
                l -> l.stream().filter(i -> i % 2 == 0).collect(Collectors.toList()),
                Collectors.toList()),
            i -> i.stream().flatMap(j -> j.stream()).collect(Collectors.toList()))));

Is there a shorter better way using solely java-8?

like image 481
Nikolas Charalambidis Avatar asked Jan 21 '19 10:01

Nikolas Charalambidis


People also ask

Is Stream API introduced in Java 8?

Introduced in Java 8, the Stream API is used to process collections of objects. A stream is a sequence of objects that supports various methods which can be pipelined to produce the desired result.

What is the use of collectors in Java 8?

collect() is one of the Java 8's Stream API's terminal methods. It allows us to perform mutable fold operations (repackaging elements to some data structures and applying some additional logic, concatenating them, etc.) on data elements held in a Stream instance.

What does collectors toList () do?

The toList() method of Collectors Class is a static (class) method. It returns a Collector Interface that gathers the input data onto a new list. This method never guarantees type, mutability, serializability, or thread-safety of the returned list but for more control toCollection(Supplier) method can be used.


2 Answers

I would just backport flatMapping. It only requires 2 methods and 1 class, with no other dependencies.

Also, when it comes time to upgrade to Java 9, you can just deprecate your version and replace any usages of it with the proper version.

The following code is taken from the JDK. I didn't write it. I have tested it with your example and it returns the same result.

class Nikollectors
{
   public static <T, U, A, R> Collector<T, ?, R> flatMapping(Function<? super T, ? extends Stream<? extends U>> mapper, Collector<? super U, A, R> downstream) {
        BiConsumer<A, ? super U> downstreamAccumulator = downstream.accumulator();
        return new CollectorImpl<>(downstream.supplier(),
            (r, t) -> {
                try (Stream<? extends U> result = mapper.apply(t)) {
                    if (result != null)
                        result.sequential().forEach(u -> downstreamAccumulator.accept(r, u));
                }
            },
            downstream.combiner(), downstream.finisher(),
            downstream.characteristics());
    }

   private static class CollectorImpl<T, A, R> implements Collector<T, A, R>
   {
        private final Supplier<A> supplier;
        private final BiConsumer<A, T> accumulator;
        private final BinaryOperator<A> combiner;
        private final Function<A, R> finisher;
        private final Set<Characteristics> characteristics;

        CollectorImpl(Supplier<A> supplier,
                      BiConsumer<A, T> accumulator,
                      BinaryOperator<A> combiner,
                      Function<A,R> finisher,
                      Set<Characteristics> characteristics) {
            this.supplier = supplier;
            this.accumulator = accumulator;
            this.combiner = combiner;
            this.finisher = finisher;
            this.characteristics = characteristics;
        }

        CollectorImpl(Supplier<A> supplier,
                      BiConsumer<A, T> accumulator,
                      BinaryOperator<A> combiner,
                      Set<Characteristics> characteristics) {
            this(supplier, accumulator, combiner, castingIdentity(), characteristics);
        }

        @Override
        public BiConsumer<A, T> accumulator() {
            return accumulator;
        }

        @Override
        public Supplier<A> supplier() {
            return supplier;
        }

        @Override
        public BinaryOperator<A> combiner() {
            return combiner;
        }

        @Override
        public Function<A, R> finisher() {
            return finisher;
        }

        @Override
        public Set<Characteristics> characteristics() {
            return characteristics;
        }
    }

   private static <I, R> Function<I, R> castingIdentity() {
       return i -> (R) i;
   }
}

Sample usage:

Map<Integer, List<Integer>> map =list.stream()
    .collect(Collectors.groupingBy(
         Collection::size,
         Nikollectors.flatMapping( // <<<
             l -> l.stream().filter(i -> i % 2 == 0),
             Collectors.toList()
        )
    )
);
like image 110
Michael Avatar answered Sep 23 '22 11:09

Michael


For just this particular case, I guess this would be a simpler version:

Map<Integer, List<Integer>> map =
        list.stream()
            .collect(Collectors.toMap(
                Collection::size,
                x -> x.stream().filter(y -> y % 2 == 0).collect(Collectors.toList())
            ));

If there would be merging involved (two collections that would have the same size), I would add a merge function that is pretty trivial:

 Map<Integer, List<Integer>> map =
        list.stream()
            .collect(Collectors.toMap(
                Collection::size,
                x -> x.stream().filter(y -> y % 2 == 0).collect(Collectors.toCollection(ArrayList::new)),
                (left, right) -> {
                    left.addAll(right);
                    return left;
                }
            ));

Otherwise, I agree with Michael in this comment, this is not that hard to back-port to java-8.

like image 28
Eugene Avatar answered Sep 22 '22 11:09

Eugene