9

I have the following code that I'd like to try to write using the java collectors.

Given 2 attributes (firstname and lastname) of a person, I'd like to get a map containing the unique firstname or lastname as a key, and the list of the corresponding persons.

Here's a set of data :

Person person1 = new Person();
person1.setFirstName("john");
person1.setLastName("doe");
person1.setUserId("user1");

Person person2 = new Person();
person2.setFirstName("doe");
person2.setLastName("frank");
person2.setUserId("user2");

Person person3 = new Person();
person3.setFirstName("john");
person3.setLastName("wayne");
person3.setUserId("user3");

List<Person> personList = new ArrayList<>();
personList.add(person1);
personList.add(person2);
personList.add(person3);

Output (as expected) is the following :

frank=[Person{userId='user2', firstName='doe', lastName='frank'}], 

john=[Person{userId='user1', firstName='john', lastName='doe'}, Person{userId='user3', firstName='john', lastName='wayne'}], 

doe=[Person{userId='user1', firstName='john', lastName='doe'}, Person{userId='user2', firstName='doe', lastName='frank'}], 

wayne=[Person{userId='user3', firstName='john', lastName='wayne'}]

And the code to populate the map :

Map<String, List<Person>> mapPersons = new HashMap<String, List<Person>>();
List<Person> listPersons;

for (Person p: personList) {
    if (mapPersons.get(p.getFirstName()) == null) {
        listPersons = new ArrayList<Person>();
        listPersons.add(p);
        mapPersons.put(p.getFirstName(), listPersons);
    } else {
        mapPersons.get(p.getFirstName()).add(p);
    }
    if (mapPersons.get(p.getLastName()) == null) {
        listPersons = new ArrayList<Person>();
        listPersons.add(p);
        mapPersons.put(p.getLastName(), listPersons);
    } else {
        mapPersons.get(p.getLastName()).add(p);
    }
}

I can't figure out how I can get either the firstname or the lastname as a key (not like in Group by multiple field names in java 8). Do I have to write my own collector?

Samuel Philipp
  • 10,631
  • 12
  • 36
  • 56
krakig
  • 1,515
  • 1
  • 19
  • 33
  • 1
    As a side note (irrelevant to your question), there are easier ways to put a value into the list if absent using the newer methods `Map.computeIfAbsent` etc. (https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#computeIfAbsent-K-java.util.function.Function-) – sfiss Jul 09 '19 at 12:27
  • True ! But that's an old code that I just took from a java 6 app! – krakig Jul 09 '19 at 12:30
  • 1
    Since every item eventually ends up as two keys, `toMap` and `groupingBy` won't work. I assume writing your own collector is the best way to go. Alternatively, duplicating every item with the firstname/lastname as key (as in Samuels answer) works too. – sfiss Jul 09 '19 at 12:35
  • 1
    Also should the title be changed to "map one item with multiple fields to multiple keys"? – sfiss Jul 09 '19 at 12:37
  • 1
    [@sfiss’ comment](https://stackoverflow.com/questions/56952524/java-stream-collect-map-one-item-with-multiple-fields-to-multiple-keys#comment100443560_56952524) is valuable as rewriting the loop to use `computeIfAbsent` may end up at simpler code than the Stream solution. – Holger Jul 09 '19 at 13:55

1 Answers1

10

You can use Stream.flatMap() and Collectors.groupingBy() with Collectors.mapping():

Map<String, List<Person>> result = personList.stream()
        .flatMap(p -> Stream.of(p.getFirstName(), p.getLastName()).map(n -> new AbstractMap.SimpleEntry<>(n, p)))
        .collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toList())));

This uses flatMap to expand all names (first and last) to their Person object and collects it afterwards.

Alternatively using Java 9 or above you could use Collectors.flatMapping():

Map<String, List<Person>> result = personList.stream()
        .collect(Collectors.flatMapping(
                p -> Stream.of(p.getFirstName(), p.getLastName()).map(n -> new AbstractMap.SimpleEntry<>(n, p)), 
                Collectors.groupingBy(Map.Entry::getKey, 
                        Collectors.mapping(Map.Entry::getValue, Collectors.toList()))));

But I don't think that this is more readable.

Adriaan Koster
  • 15,870
  • 5
  • 45
  • 60
Samuel Philipp
  • 10,631
  • 12
  • 36
  • 56