Java 8 Stream "collect and group by" objects that map to multiple keys [duplicate]
Asked Answered
C

2

7

I have the following objects:

public class Item {
    String value;
    List<Person> owners;
    Person creator;
}

public class Person {
    String name;
    int id;
    Person manager;
}

now i have the a list containing 3 Item objects:

i1 -> {value="1", owners=[p1, p2, p3], creator=p4}
i2 -> {value="2", owners=[p2, p3], creator=p5}
i3 -> {value="3", owners=[p5], creator=p1}

the Person objects are as follows:

p1 -> {manager=m1, ...}
p2 -> {manager=m2, ...}
p3 -> {manager=m3, ...}
p4 -> {manager=m2, ...}
p5 -> {manager=m1, ...}

I want to group the stream of Item objects based on managers of the owners and creator. The result Map<Person, List<Item>> should look like:

{
  m1: [i1, i2, i3],
  m2: [i1, i2],
  m3: [i1, i2]
}

I think using Stream and Collector APIs, I can make the map from Item to managers, Map<Item, List<Person>> first, then reverse the mapping. But is there any way to make the mapping I want using Stream and Collectors only?

Cylinder answered 12/10, 2015 at 17:11 Comment(0)
L
22

I think, this is only possible with an intermediate “pair” value to remember the association between a person/manager and the originating item. In absence of a standard pair type in Java’s standard API, we have to resort to Map.Entry which comes closest to a Pair type:

Map<Person, List<Item>> map = list.stream()
  .flatMap(item->item.getOwners().stream()
    .map(p->new AbstractMap.SimpleEntry<>(p.getManager(), item)))
  .collect(Collectors.groupingBy(Map.Entry::getKey,
    Collectors.mapping(Map.Entry::getValue, Collectors.toList())));

after improving it using import static we get

Map<Person, List<Item>> map = list.stream()
  .flatMap(item->item.getOwners().stream().map(p->new SimpleEntry<>(p.getManager(), item)))
  .collect(groupingBy(Map.Entry::getKey, mapping(Map.Entry::getValue, toList())));

It results in

m1: [i1,i3]
m3: [i1,i2]
m2: [i1,i2]

which differs because first, the standard map has no defined ordering, second, I think you made a mistake regarding you expectation as m1 is not associated with i2 in your example data.

Lendlease answered 12/10, 2015 at 18:6 Comment(2)
I also need the mapping for creators of the list of Items. m1 -> p5 -> creator of i2. But I think I can modify your solution a little bit and do .map(item -> item.getCreator()...) and then merge the corresponding List<Item> for the same Person key (manager).Cylinder
Actually what I also want is mapping based multiple properties. It is answered here #28508753Cylinder
F
4

Such scenarios are well supported by my free StreamEx library which enhances standard Stream API:

Map<Person, List<Item>> map = StreamEx.of(list) // create an enhanced stream of Item
                // create a stream of Entry<Item, manager>
                .cross(item -> item.getOwners().stream().map(Person::getManager))
                // swap keys and values to get stream of Entry<manager, Item>
                .invert()
                .grouping();

Internally it's similar to @Holger solution.

Fachini answered 12/10, 2015 at 21:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.