How to make a Map of Map using Collectors

Lovegiver :

I'm blocked in using Java Functional Programing for Map building. In my use case, that is just an example, I would like to build a Map<Integer, Map<Integer, List<Person>>> starting from a List<Person> .

The first step is easy : I build a first Map<Integer, List<person>>.

Then I want to add a new grouping level so it will produce a new Map : Map<Integer, Map<Integer, List<person>>>.

The point is : how to correctly use all Collectors capabilities (groupingBy, mapping, maybe something else) to add a new grouping level ? All my tries failed : compiler always tells me that all my provided functions do not fit with what it expects.

Here is the code, briefly :

First a Person class and then a method to quicly build a List :

public class Person {

  private long id;
  private String firstName;
  private String lastName;
  private Date birthDate;
  private int alea;
  ...

This method (located in a DataBuilder class) just builds a list of Person to provide datas to the Main class :

public List<Person> getPersons() {

        List<Person> persons = new ArrayList<>();
        Supplier<Person> person = Person::new;

        for (int cpt = 0; cpt < 10; cpt++) {
            Person p = person.get();
            p.setId(cpt);
            p.setFirstName("fn" + cpt);
            p.setLastName("ln" + cpt);
            p.setBirthDate(new Date(119, cpt, 10 + cpt));
            p.setAlea(cpt % 2);
            persons.add(p);
        }

        return persons;

    }

In the Main, building the first level of Map is not a problem :

public static void main(String[] args) {

    DataBuilder db = new DataBuilder();

    List<Person> persons = db.getPersons();

    Map<Integer, List<Person>> mapAleaPerson = persons.stream()
                .collect(Collectors.groupingBy(Person::getAlea,
                        Collectors.mapping(p -> p, Collectors.toList())));

        mapAleaPerson.forEach((k,v) -> System.out.printf("%n%s contains %s%n", k, v));

Result is OK in the console :

0 contains [Person [id=0, firstName=fn0, lastName=ln0, alea=0], Person [id=2, firstName=fn2, lastName=ln2, alea=0], Person [id=4, firstName=fn4, lastName=ln4, alea=0], Person [id=6, firstName=fn6, lastName=ln6, alea=0], Person [id=8, firstName=fn8, lastName=ln8, alea=0]]

1 contains [Person [id=1, firstName=fn1, lastName=ln1, alea=1], Person [id=3, firstName=fn3, lastName=ln3, alea=1], Person [id=5, firstName=fn5, lastName=ln5, alea=1], Person [id=7, firstName=fn7, lastName=ln7, alea=1], Person [id=9, firstName=fn9, lastName=ln9, alea=1]]

Now I would like to add an new grouping level. I've chosen BirthDate's year, that is another property of a Person object.

Basically, I tried somthing like this :

Map<Integer,Map<Integer,List<Person>>> mapByYearThenAleaPerson = persons.stream()
                .collect(Collectors.groupingBy(p -> p.getBirthDate().getYear(),
                        Collectors.toMap(Collectors.groupingBy(Person::getAlea,
                        Collectors.mapping(p -> p, Collectors.toList())))));

Casting doesn't help :

@SuppressWarnings("unchecked")
        Map<Integer,Map<Integer,List<Person>>> mapByYearThenAleaPerson = persons.stream()
                .collect(Collectors.groupingBy(p -> p.getBirthDate().getYear(),
                        Collectors.toMap((Function<Integer, List<Person>>) Collectors.groupingBy(Person::getAlea,
                        Collectors.mapping(p -> p, Collectors.toList())))));

I also tried with Collectors.mapping instead of Collectors.toMap :

Map<Integer,Map<Integer,List<Person>>> mapByYearThenAleaPerson = persons.stream()
                .collect(Collectors.groupingBy(p -> p.getBirthDate().getYear(),
                        Collectors.mapping((Function<Integer, List<Person>>) Collectors.groupingBy(Person::getAlea,
                        Collectors.mapping(p -> p, Collectors.toList())))));

Each time the compiler returns error because of uncompliant Function type.

The only way I've found to add the new grouping level was with old Java code and external loop adding and grouping datas in a Map of Map.

It's desapointing to embed FP in old Java!!

Have someone any idea of doing all of that with pure FP ?


After @eran answer (that works great), here's what I've done and its result :

Map<Integer, Map<Integer, List<Person>>> mapByYearThenAleaPerson = persons.stream().collect(
                Collectors.groupingBy(p -> p.getBirthDate().getYear() + 1900, Collectors.groupingBy(Person::getAlea)));


{2018={0=[Person [id=0, firstName=fn0, lastName=ln0, alea=0], Person [id=2, firstName=fn2, lastName=ln2, alea=0], Person [id=4, firstName=fn4, lastName=ln4, alea=0]], 1=[Person [id=1, firstName=fn1, lastName=ln1, alea=1], Person [id=3, firstName=fn3, lastName=ln3, alea=1]]}, 2019={0=[Person [id=6, firstName=fn6, lastName=ln6, alea=0], Person [id=8, firstName=fn8, lastName=ln8, alea=0]], 1=[Person [id=5, firstName=fn5, lastName=ln5, alea=1], Person [id=7, firstName=fn7, lastName=ln7, alea=1], Person [id=9, firstName=fn9, lastName=ln9, alea=1]]}}

What is really surprenising, is the simplicity of nested groupingBy statement compared to the way of builging the first Map itself.

Why don't we need to specify mapping and Collectors.toList() anymore ?

Eran :

Use nested groupingBy:

Map<Integer,Map<Integer,List<Person>>> mapByYearThenAleaPerson = 
    persons.stream()
           .collect(Collectors.groupingBy(p -> p.getBirthDate().getYear(),
                                          Collectors.groupingBy(Person::getAlea)));

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=129161&siteId=1