I have a Person class like below:
public class Person {
private int id;
private String name;
private int score;
// constructor, getters, etc.
}
And I have a list of Persone
objects and I want to group them into a Map
.
Main class:
public class Main {
public static void main(String[] args) {
Person person1 = new Person(1, "John", 4);
Person person2 = new Person(2, "John", 3);
Person person3 = new Person(2, "John", 4);
List<Person> personList = new ArrayList<>();
personList.add(person1);
personList.add(person2);
personList.add(person3);
Map<String, List<Person>> personByName = personList.stream()
.collect(groupingBy(Person::getName));
System.out.println(personByName);
}
}
It gives the result:
{John=[Person{id=1, name='John', score=4},
Person{id=2, name='John', score=3},
Person{id=2, name='John', score=4}]}
How can I group by person name
, but still distinguish two people by id
by keeping them separatelly inside the map?
I want the result to look like this:
{John=[Person{id=1, name='John', score=4}],
John=[Person{id=2, name='John', score=3}, Person{id=2, name='John', score=4}]}
How can I group by person
name
, but still distinguish two people byid
by keeping them separatelly inside the map?
It's inherently impossible to have two identical keys in a Map
.
But there are some options to group the data in such a way.
Note: ID is usually expected to be unique, but in your example it's not, so I will blind on it. Although we might use score as the second property for grouping, I'll proceed with grouping person objects by ID
To group people by name
and then by id
you can create a nested map Map<String,Map<Integer,List<Person>>>
, which will associate each unique name
with a map that allows to retrieve a having a particular id
(and obviously the same name).
That's how it might be implemented:
List<Person> personList = new ArrayList<>();
Collections.addAll(personList, // with Java 9+ use List.of() instead
new Person(1, "John", 4),
new Person(2, "John", 3),
new Person(2, "John", 4)
);
Map<String, Map<Integer, List<Person>>> personByNameAndId = personList.stream()
.collect(Collectors.groupingBy(
Person::getName,
Collectors.groupingBy(Person::getId)
));
personByNameAndId.forEach((name, personById) -> {
System.out.println(name);
personById.forEach((id, people) -> people.forEach(p -> System.out.println("id " + id + " -> " + p)));
});
Output:
John
id 1 -> Person{id=1, name='John', score=4}
id 2 -> Person{id=2, name='John', score=3}
id 2 -> Person{id=2, name='John', score=4}
It's worth to mention that it's advisable to avoid using nested collections because they are cumbersome and you might introduce a bug if you're not attentive enough when dialing with. For example, with map of maps, you can accidentally swap the order of keys while invoking get()
, and because Map.get()
expects an argument of type Object
the code would still compile.
Another option would be to introduce an object that would hold the name
and id
of a person. It can be implemented either as a class (which would be the only option with Java 8):
public class IdName {
private final int id;
private final String name;
public IdName(Person p) {
this(p.getId(), p.getName());
}
// getters, all-args-constructor, equals/hashcode
}
Or as a Java 16 record:
public record IdName(int id, String name) {
public IdName(Person p) {
this(p.getId(), p.getName());
}
}
In order to facilitate convention Person
into IdName
you can introduce either a constructor of IdName
which expects an instance of Person
(as shown above), or convenience method toIdName()
in the Person
class.
That how to group people using this custom object, a key and retrieve from the map a List<Person>
that share name
and id
with the given person:
Map<IdName, List<Person>> personByNameAndId = personList.stream()
.collect(Collectors.groupingBy(IdName::new));
Person john = new Person(2, "John", 0);
personByNameAndId.get(new IdName(john))
.forEach(System.out::println);
Output:
Person{id=2, name='John', score=3}
Person{id=2, name='John', score=4}