4

How can I reduce the Map<X, List<String>> grouping by the X.p and join all the list values at the same time, so that I have Map<Integer, List<String>> at the end?

This is what I've tried so far:

class X {
 int p;
 int q;
 public X(int p, int q) { this.p = p; this.q = q; }
}
Map<X, List<String>> x = new HashMap<>();
x.put(new X(123,5), Arrays.asList("A","B"));
x.put(new X(123,6), Arrays.asList("C","D"));
x.put(new X(124,7), Arrays.asList("E","F"));
Map<Integer, List<String>> z = x.entrySet().stream().collect(Collectors.groupingBy(
 entry -> entry.getKey().p, 
 mapping(Map.Entry::getValue, 
 reducing(new ArrayList<>(), (a, b) -> { a.addAll(b); return a; }))));
System.out.println("z="+z);

But the result is: z={123=[E, F, A, B, C, D], 124=[E, F, A, B, C, D]}.

I want to have z={123=[A, B, C, D], 124=[E, F]}

asked Jul 27, 2015 at 9:50

4 Answers 4

5

Here's one way to do it using two Stream pipelines :

Map<Integer, List<String>> z = 
// first process the entries of the original Map and produce a 
// Map<Integer,List<List<String>>>
 x.entrySet()
 .stream()
 .collect(Collectors.groupingBy(entry -> entry.getKey().p, 
 mapping(Map.Entry::getValue,
 toList())))
// then process the entries of the intermediate Map and produce a 
// Map<Integer,List<String>>
 .entrySet()
 .stream()
 .collect (toMap (Map.Entry::getKey,
 e -> e.getValue()
 .stream()
 .flatMap(List::stream)
 .collect(toList())));

Java 9 is supposed to add a flatMapping Collector, that would make your life easier (I learned about this new feature thanks to Holger).

Output :

z={123=[A, B, C, D], 124=[E, F]}
answered Jul 27, 2015 at 10:08

3 Comments

Do you think it can't be done with a stream being collected by a single groupingBy or toMap (having some combination of mapping, reducing, flatMap...) in Java 8?
@tafit3 Currently I can't think of a way to do it in a single Stream pipeline.
It’s actually not that hard to do; there is only one mistake in the OP’s code...
3

You are using the reducing collector incorrectly. The first argument must be an identity value to the reduction operation. But you are modifying it by adding values to it, which perfectly explains the result: all values are added to the same ArrayList which is expected to be the invariant identity value.

What you want to do is a Mutable reduction and Collectors.reducing is not appropriate for that. You may create an appropriate collector using the method Collector.of(...):

Map<Integer, List<String>> z = x.entrySet().stream().collect(groupingBy(
 entry -> entry.getKey().p, Collector.of(
 ArrayList::new, (l,e)->l.addAll(e.getValue()), (a,b)->{a.addAll(b);return a;})));
answered Jul 27, 2015 at 13:32

Comments

3

There is a way to achieve that in one run by writing your own Collector:

Map<Integer, List<String>> z = x.entrySet().stream().collect(
 Collectors.groupingBy(entry -> entry.getKey().p,
 Collectors.mapping(Entry::getValue, 
 Collector.of(ArrayList::new, (a, b) -> a.addAll(b), (a, b) -> {
 a.addAll(b);
 return a;
 })
 )
 )
);
answered Jul 27, 2015 at 10:35

Comments

3

Using the EntryStream class of my StreamEx library such tasks can be solved quite easily:

Map<Integer, List<String>> z = EntryStream.of(x)
 .mapKeys(k -> k.p)
 .flatMapValues(List::stream)
 .grouping();

Internally it's transformed to something like this:

Map<Integer, List<String>> z = x.entrySet().stream()
 .map(e -> new AbstractMap.SimpleImmutableEntry<>(e.getKey().p, e.getValue()))
 .<Entry<Integer, String>>flatMap(e -> e.getValue().stream()
 .map(s -> new AbstractMap.SimpleImmutableEntry<>(e.getKey(), s)))
 .collect(Collectors.groupingBy(e -> e.getKey(), 
 Collectors.mapping(e -> e.getValue(), Collectors.toList())));

So it's actually a single stream pipeline.

If you don't want to use the third-party code, you can simplify the above version a little:

Map<Integer, List<String>> z = x.entrySet().stream()
 .<Entry<Integer, String>>flatMap(e -> e.getValue().stream()
 .map(s -> new AbstractMap.SimpleEntry<>(e.getKey().p, s)))
 .collect(Collectors.groupingBy(e -> e.getKey(), 
 Collectors.mapping(e -> e.getValue(), Collectors.toList())));

Though it still looks ugly.

Finally please note that in JDK9 there's new standard collector called flatMapping which can be implemented in the following way:

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 Collector.of(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().toArray(new Collector.Characteristics[0]));
}

Using this collector, your task can be solved simpler without additional libraries:

Map<Integer, List<String>> z = x.entrySet().stream()
 .map(e -> new AbstractMap.SimpleImmutableEntry<>(e.getKey().p, e.getValue()))
 .collect(Collectors.groupingBy(e -> e.getKey(), 
 flatMapping(e -> e.getValue().stream(), Collectors.toList())));
answered Jul 27, 2015 at 10:55

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.