Java 8: grouping a collection by field and smoothing and merging the collection as a display value using a stream?

My class has two fields:

  • MyKey is the key I want to group by
  • Set<MyEnum> is the set that I want to smooth and merge.

I have a list of such objects, and I want to get Map<MyKey, Set<MyEnum> , from which the value is combined with all myEnums objects using this key.

For example, if I have three objects:

  • myKey: key1, myEnums: [E1]
  • myKey: key1, myEnums: [E2]
  • myKey: key2, myEnums: [E1, E3]

Expected Result:

key1 => [E1, E2], key2 => [E1, E3]

I came up with this code:

 Map<MyKey, Set<MyEnum>> map = myObjs.stream() .collect(Collectors.groupingBy( MyType::getMyKey, Collectors.reducing( new HashSet<MyEnum>(), MyType::getMyEnums, (a, b) -> { a.addAll(b); return a; }))); 

There are two problems:

  • HashSet inside the shortcut seems to be shared between all keys. The actual result of the above example is key1 => [E1, E2, E3], key2 => [E1, E2, E3] . Why is this so?

  • Even if this code works, it looks ugly, especially in terms of shorthand, that I have to handle the logic of building the combined collection manually. Is there a better way to do this?

Thanks!

+5
source share
2 answers

Note that you only create one authentication object: new HashSet<MyEnum>() .

BinaryOperator that you supply as the third argument should be idempotent , just like normal math operators, for example. x = y + z does not change the values โ€‹โ€‹of y and z .

This means that you need to combine the two input sets a and b without updating them.

In addition, when working with enumerations, you should use EnumSet , not HashSet .

 Map<MyKey, Set<MyEnum>> map = myObjs.stream() .collect(Collectors.groupingBy( MyType::getMyKey, Collectors.reducing( EnumSet.noneOf(MyEnum.class), // <-- EnumSet MyType::getMyEnums, (a, b) -> { EnumSet<MyEnum> c = EnumSet.copyOf(a); // <-- copy c.addAll(b); return c; }))); 

UPDATE

A shorter, more optimized version that should not create new sets when accumulating the result:

 Map<MyKey, Set<MyEnum>> map = myObjs.stream() .collect(Collectors.groupingBy( MyType::getMyKey, Collector.of( () -> EnumSet.noneOf(MyEnum.class), (r, myObj) -> r.addAll(myObj.getMyEnums()), (r1, r2) -> { r1.addAll(r2); return r1; } ))); 
+5
source

Not perfect, but using a volatile container makes it pretty easy to understand.

 myObjs.stream() .collect(groupingBy(MyType::getMyKey) .entrySet().stream() .collect(toMap( Map.Entry::getKey, e -> e.getValue() .stream() .flatMap(v -> v.getMyEnums().stream()) .collect(toSet()) ) 

Collectors.mapping (Function, Collector) is so close to what you want to do here if it were Collectors.flatMapping

EDIT: until java 9 appears, there is a convenient implementation of flatMapping in this answer . With it, our solution looks like this:

 myObjs.stream() .collect( groupingBy(MyType::getMyKey, flatMapping(v -> v.getMyEnums().stream(), toSet()) ); 
0
source

All Articles