Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 mapping to sub list entries of a collection using streams and collectors

I have a collection of Person objects:.

public class Person {

  String name;

  ChildrenListHolder childrenListHolder;
}

public class ChildrenListHolder {
   List<Children> children;
}

public class Children {
   String childrensName;
}

(The entity structure is given by third party.)

Now, I need a Map<String,List<Person>> childrensName -> person-list

For example (simplified):

Person father: {name: "John", childrensListHolder -> {"Lisa", "Jimmy"}}
Person mother: {name: "Clara", childrensListHolder -> {"Lisa", "Paul"}}
Person george: {name: "George", childrensListHold -> "Paul"}}

The map, I need is

Map<String, List<Person>> map: {"Lisa"  -> {father, mother},
                                "Jimmy" -> {father},
                                "Paul"  -> {mother, george}}

I can do that with a bunch of for's and if's. But how can I do this using streams and collectors. I have tried many approaches, but I cannot get the expected result. TIA.

like image 744
t777 Avatar asked Mar 28 '16 20:03

t777


People also ask

Which method of streams API is used to collect Stream elements into a List set?

3.1. The toList collector can be used for collecting all Stream elements into a List instance.

Does collectors toList () return ArrayList?

For instance, Collectors. toList() method can return an ArrayList or a LinkedList or any other implementation of the List interface. To get the desired Collection, we can use the toCollection() method provided by the Collectors class.

What does collect collectors toList ()) do?

Collectors toList() method in Java with ExamplesIt 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

Given a List<Person> persons, you can have the following

Map<String,List<Person>> map =
    persons.stream()
           .flatMap(p -> p.childrenListHolder.children.stream().map(c -> new AbstractMap.SimpleEntry<>(c, p)))
           .collect(Collectors.groupingBy(
             e -> e.getKey().childrensName,
             Collectors.mapping(Map.Entry::getValue, Collectors.toList())
           ));

This is creating a Stream over the persons. Then each person is flat mapped by a tuple holding the child and the person for each child. Finally, we group by the child name and collect all the persons into a list.

Sample code assuming there are appropriate constructors:

public static void main(String[] args) {
    List<Person> persons = Arrays.asList(
        new Person("John", new ChildrenListHolder(Arrays.asList(new Children("Lisa"), new Children("Jimmy")))),
        new Person("Clara", new ChildrenListHolder(Arrays.asList(new Children("Lisa"), new Children("Paul")))),
        new Person("George", new ChildrenListHolder(Arrays.asList(new Children("Paul"))))
    );

    Map<String,List<Person>> map =
        persons.stream()
               .flatMap(p -> p.childrenListHolder.children.stream().map(c -> new AbstractMap.SimpleEntry<>(c, p)))
               .collect(Collectors.groupingBy(
                 e -> e.getKey().childrensName,
                 Collectors.mapping(Map.Entry::getValue, Collectors.toList())
               ));

    System.out.println(map);
}
like image 166
Tunaki Avatar answered Oct 10 '22 03:10

Tunaki


I can do that with a bunch of for's and if's.

I know that you asked for a stream/collectors solution, but in any case a nested for loop using Map#computeIfAbsent works fine too:

Map<String, List<Person>> map = new HashMap<>();
for(Person p : persons) {
    for(Children c : p.childrenListHolder.children) {
        map.computeIfAbsent(c.childrensName, k -> new ArrayList<>()).add(p);
    }
}

and this written using the new forEach method introduced on collections:

Map<String, List<Person>> map = new HashMap<>();
persons.forEach(p -> p.childrenListHolder.children.forEach(c -> map.computeIfAbsent(c.childrensName, k -> new ArrayList<>()).add(p)));

Of course it's not a one-liner nor easy parallelizable like in Tunaki's solution (+1), but you don't need a "bunch" of if's to achieve that too (and you avoid also the creation of temporary map entries instances).

like image 44
Alexis C. Avatar answered Oct 10 '22 01:10

Alexis C.