Nabil Sham Nabil Sham - 6 months ago 12
Java Question

What is a better way to break java list in Map of Maps?

We have

List<persons> persons;


and we need

Map<age,Map<income,Person>> results


the way I am doing it now is :

ages.stream().forEach(a -> {
Map<Integer,Person> tmpMap = new HashMap<>();
incomes.stream().forEach(i -> {
Person p = persons.stream().filter(
u -> u.getAge() == a.getAge() &&
u.getIncome() == i.getIncome())
.findAny().orElse(null);
tmpMap.put(i.getIncome(), p);
});
returns.put(a.getAge(),tmpMap);
});


it seems like there should be a better way of doing this.

Answer

This looks like it works.

List<Person> people = Arrays.asList(
        new Person("One", 21, 100),
        new Person("Two", 21, 75),
        new Person("Three", 42, 100),
        new Person("Four", 42, 120),
        new Person("Five", 9, 100)
);
Map<Integer, Map<Integer, Person>> map = people.stream()
        // Gather all ages into a Map<Age,List<Person>>
        .collect(Collectors.groupingBy(Person::getAge))
        // Walk that transient Map.
        .entrySet().stream()
        .collect(Collectors.toMap(
                // Key is the age.
                Map.Entry::getKey,
                // Value is a Map<income,person>
                e -> e.getValue()
                // Roll each of the same age into a Map<Income,Person>
                .stream().collect(
                        Collectors.toMap(
                                // Key is income.
                                Person::getIncome,
                                // Value is the Person.
                                Function.identity()
                        ))));

I roll your list into a Map<Age,List<Person>> using a groupingBy and then stream it's entrySet and collect that into the final form.

This will fail if two people of the same age have the same income because that will violate the inner Map. Use Alexander's suggestion if you are happy with the natural enhancement of generating a Map<Integer, Map<Integer, List<Person>>>.

Added

@Holger has pointed out in a comment that this can be done in a much simpler and more elegant way. Please use this form instead/

Map<Integer, Map<Integer, Person>> map2 = people.stream()
        .collect(
                Collectors.groupingBy(
                        Person::getAge,
                        Collectors.toMap(Person::getIncome, Function.identity())));

FYI - Here's the Person class I used. Note the equals and hashcode are implemented.

class Person {
    private final String name;
    private final int age;
    private final int income;


    public Person(String name, int age, int income) {
        this.name = name;
        this.age = age;
        this.income = income;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public int getIncome() {
        return income;
    }

    @Override
    public String toString() {
        return "Person{" + "name=" + name + ", age=" + age + ", income=" + income + '}';
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 59 * hash + Objects.hashCode(this.name);
        hash = 59 * hash + this.age;
        hash = 59 * hash + this.income;
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Person other = (Person) obj;
        if (this.age != other.age) {
            return false;
        }
        if (this.income != other.income) {
            return false;
        }
        if (!Objects.equals(this.name, other.name)) {
            return false;
        }
        return true;
    }
}