Bick
Bick

Reputation: 18521

Java stream - groupingBy by a nested list (list in a second order)

I have the following data structure -

List of Students that each holds a lists of States that each holds a list of cities.

public class Student {
    private int id;
    private String name;
    private List<State> states = new ArrayList<>();
}

public class State {
    private int id;
    private String name;
    private List<City> Cities = new ArrayList<>();
}

public class City {
    private int id;
    private String name;
}

I want to get the following.

Map<String, Students> citiesIdsToStudensList;

I write the following

Map<Integer, List<Integer>> statesToStudentsMap = students.stream()
            .flatMap(student -> student.getStates().stream())
            .flatMap(state -> state.getCities().stream())
            .collect(Collectors.groupingBy(City::getId, Collectors.mapping(x -> x.getId(), Collectors.toList())));

But it doesn't get me the result I want.

Upvotes: 8

Views: 4550

Answers (2)

Holger
Holger

Reputation: 298153

In addition to Tunaki’s answer, you can simplify it as

Map<Integer, List<Student>> citiesIdsToStudentsList =
    students.stream()
        .flatMap(student -> student.getStates().stream()
            .flatMap(state -> state.getCities().stream())
            .map(state -> new AbstractMap.SimpleEntry<>(student, state.getId())))
        .collect(Collectors.groupingBy(
            Map.Entry::getValue,
            Collectors.mapping(Map.Entry::getKey, Collectors.toList())
        ));

It utilizes the fact that you are not actually interested in State objects, so you can flatMap them directly to the desired City objects, if you do it right within the first flatMap operation. Then, by performing the State.getId operation immediately when creating the Map.Entry, you can simplify the actual collect operation.

Upvotes: 0

Tunaki
Tunaki

Reputation: 137084

Using the Stream API, you'll need to flat map twice and map each intermediate student and city into a tuple that is capable of holding the student.

Map<Integer, List<Student>> citiesIdsToStudentsList =
    students.stream()
            .flatMap(student -> student.getStates().stream().map(state -> new AbstractMap.SimpleEntry<>(student, state)))
            .flatMap(entry -> entry.getValue().getCities().stream().map(city -> new AbstractMap.SimpleEntry<>(entry.getKey(), city)))
            .collect(Collectors.groupingBy(
                entry -> entry.getValue().getId(),
                Collectors.mapping(Map.Entry::getKey, Collectors.toList())
            ));

However, it would maybe be cleaner to use nested for loops here:

Map<Integer, List<Student>> citiesIdsToStudentsList = new HashMap<>();
for (Student student : students) {
    for (State state : student.getStates()) {
        for (City city : state.getCities()) {
            citiesIdsToStudentsList.computeIfAbsent(city.getId(), k -> new ArrayList<>()).add(student);
        }
    }
}

This leverages computeIfAbsent to populate the map and creates a list of each student with the same city id.

Upvotes: 6

Related Questions