Reputation: 113
I am new to Java and I am trying to merge multiple maps with string as key and list as values to produce a new Map.
public class Student {
private String name;
private String country;
//Setters and Getters
}
Now I have an util class to add students to the list based on their country.
public class MapAdder {
static Map<String, List<Student>> studentMap =
new LinkedHashMap<String, List<Student>>();
public static void addToMap(String key, Student student) {
studentMap.computeIfAbsent(key,
k -> new LinkedList<Student>()).add(student);
}
public static Map<String, List<Student>> getStudentMap() {
return studentMap;
}
public static void clearStudentMap() {
studentMap.clear();
}
}
Main Method
public static void main(String[] args) {
Map<String, List<Student>> studentMap1;
Map<String, List<Student>> studentMap2;
Map<String, List<Student>> studentMap3;
MapAdder.addToMap("India", new Student("Mounish", "India"));
MapAdder.addToMap("USA", new Student("Zen", "USA"));
MapAdder.addToMap("India", new Student("Ram", "India"));
MapAdder.addToMap("USA", new Student("Ronon", "USA"));
MapAdder.addToMap("UK", new Student("Tony", "UK"));
studentMap1 = MapAdder.getStudentMap();
MapAdder.clearStudentMap();
MapAdder.addToMap("India", new Student("Rivar", "India"));
MapAdder.addToMap("UK", new Student("Loki", "UK"));
MapAdder.addToMap("UK", new Student("Imran", "UK"));
MapAdder.addToMap("USA", new Student("ryan", "USA"));
studentMap2 = MapAdder.getStudentMap();
MapAdder.clearStudentMap();
Map<String, List<Student>> map3 = Stream.of(studentMap1, studentMap2)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Entry::getKey,
Entry::getValue
));
}
But when I try to merge both the maps I am getting empty map. Actually, I need to have a map with three keys (India, UK, USA) and their values that are list from multiple maps to be merged w.r.t keys.
Upvotes: 1
Views: 2144
Reputation: 40057
The main problem is that you keep clearing the shared list. Independent lists need to be created.
But there is a much easier way to add the values than to use your MapAdder
class. Remember that the country
is also part of the student class. So just extract that and create the map using streams.
Now create studentMap1
List<Student> list1 = List.of(
new Student("Mounish", "India"),
new Student("Zen", "USA"),
new Student("Ram", "India"),
new Student("Ronon", "USA"),
new Student("Tony", "UK"));
Map<String, List<Student>> studentMap1 =
list1.stream().collect(Collectors.groupingBy(Student::getCountry));
studentMap1.entrySet().forEach(System.out::println);
prints
USA=[{Zen, USA}, {Ronon, USA}]
UK=[{Tony, UK}]
India=[{Mounish, India}, {Ram, India}]
Now create studentMap2
List<Student> list2 = List.of(
new Student("Rivar", "India"),
new Student("Loki", "UK"),
new Student("Imran", "UK"),
new Student("ryan", "USA"));
Map<String, List<Student>> studentMap2 =
list2.stream().collect(Collectors.groupingBy(Student::getCountry));
studentMap2.entrySet().forEach(System.out::println);
Prints
USA=[{ryan, USA}]
UK=[{Loki, UK}, {Imran, UK}]
India=[{Rivar, India}]
Now that you have the maps, you can create the combined map the same way. Just use the values of each map and then stream them to get the student instances.
Map<String, List<Student>> map3 = Stream.of(studentMap1,studentMap2)
.map(Map::values) // values which is a collection of lists
.flatMap(Collection::stream) // flat map the two collections
.flatMap(Collection::stream) // flat map the lists to just
// a stream of students
.collect(Collectors.groupingBy(Student::getCountry));
map3.entrySet().forEach(System.out::println);
Prints
USA=[{Zen, USA}, {Ronon, USA}, {ryan, USA}]
UK=[{Tony, UK}, {Loki, UK}, {Imran, UK}]
India=[{Mounish, India}, {Ram, India}, {Rivar, India}]
You were fortunate that the Map key was included as part of the Student class. But let's assume that the key was independent of the class. Then you could use your mapAdder
to build the original maps. And the final map could be created using the following with a merge
function for merging duplicate keys.
Map<String, List<Student>> map4 =
Stream.of(studentMap1, studentMap2)
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Entry::getKey,
e -> new ArrayList<>(e.getValue),
(lst1, lst2) -> {lst1.addAll(lst2); return lst1;}));
The student class with getters and setters and toString
class Student {
private String name;
private String country;
public Student(String name, String country) {
this.name = name;
this.country = country;
}
public String getName() {
return name;
}
public String getCountry() {
return country;
}
@Override
public String toString() {
return String.format("{%s, %s}", name, country);
}
}
Upvotes: 0
Reputation: 51643
First, remove from your code the following calls:
MapAdder.clearStudentMap();
you are clearing the studentMap1
and studentMap2
.
When you do:
studentMap1 = MapAdder.getStudentMap();
you get the memory reference in which the student Map
is stored. When you call the clear
method on that map
studentMap.clear();
you will clear all the Map
entries stored on that same memory reference. In other words, the following statement
studentMap1 = MapAdder.getStudentMap();
does not create a copy of the student Map
, instead it just saves on the variable studentMap1
the memory reference to that Map.
Your Stream method is almost right, change it to:
Map<String, List<Student>> map3 = Stream.of(studentMap1, studentMap2)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> new ArrayList<>(e.getValue()),
(left, right) -> { left.addAll(right); return left; }
));
You need also to add the strategy to be used to deal with the duplicate keys (i.e., the mergeFunction
parameter of the Collectors.toMap
method). In case of duplicated keys we add the Map values into the list of the left key.
Btw drop some of those helper methods IMO they obfuscate the code, and make the addToMap
method more generic by passing the Map
itself as parameter, so that you can reuse that method with different mappers, namely:
public class MapAdder {
public static void addToMap(Map<String, List<Student>> studentMap,
String key, Student student) {
studentMap.computeIfAbsent(key,
k -> new LinkedList<Student>()).add(student);
}
public static void main(String[] args) {
Map<String, List<Student>> studentMap1 = new LinkedHashMap<>();
Map<String, List<Student>> studentMap2 = new LinkedHashMap<>();
Map<String, List<Student>> studentMap3;
MapAdder.addToMap(studentMap1, "India", new Student("Mounish", "India"));
MapAdder.addToMap(studentMap1, "USA", new Student("Zen", "USA"));
MapAdder.addToMap(studentMap1, "India", new Student("Ram", "India"));
MapAdder.addToMap(studentMap1, "USA", new Student("Ronon", "USA"));
MapAdder.addToMap(studentMap1, "UK", new Student("Tony", "UK"));
MapAdder.addToMap(studentMap2, "India", new Student("Rivar", "India"));
MapAdder.addToMap(studentMap2, "UK", new Student("Loki", "UK"));
MapAdder.addToMap(studentMap2, "UK", new Student("Imran", "UK"));
MapAdder.addToMap(studentMap2, "USA", new Student("ryan", "USA"));
Map<String, List<Student>> map3 = Stream.of(studentMap1, studentMap2)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> new ArrayList<>(e.getValue()),
(left, right) -> { left.addAll(right); return left; }
));
}
}
Upvotes: 2
Reputation:
When creating a HashMap
instance, you can override its put
and putAll
methods, so that they don't replace existing values, but append them, i.e. merge lists of values for the same keys:
Map<String, List<Student>> studentMap = new HashMap<>() {
@Override
public List<Student> put(String key, List<Student> value) {
if (this.containsKey(key)) {
List<Student> val = this.get(key);
val.addAll(value);
return val;
} else {
return super.put(key, new ArrayList<>(value));
}
}
@Override
public void putAll(Map<? extends String, ? extends List<Student>> m) {
Iterator<? extends Entry<? extends String, ? extends List<Student>>>
iterator = m.entrySet().iterator();
while (iterator.hasNext()) {
Entry<? extends String, ? extends List<Test.Student>>
e = iterator.next();
this.put(e.getKey(), e.getValue());
}
}
};
studentMap.put("India", List.of(new Student("Mounish", "India")));
studentMap.put("USA", List.of(new Student("Zen", "USA")));
studentMap.putAll(Map.of(
"India", List.of(new Student("Ram", "India")),
"USA", List.of(new Student("Ronon", "USA")),
"UK", List.of(new Student("Tony", "UK"))));
studentMap.putAll(Map.of(
"India", List.of(new Student("Rivar", "India")),
"UK", List.of(new Student("Loki", "UK"))));
studentMap.putAll(Map.of(
"UK", List.of(new Student("Imran", "UK")),
"USA", List.of(new Student("ryan", "USA"))));
studentMap.forEach((k, v) -> System.out.println(k + "=" + v));
// USA=[Zen:USA, Ronon:USA, ryan:USA]
// UK=[Tony:UK, Loki:UK, Imran:UK]
// India=[Mounish:India, Ram:India, Rivar:India]
If you don't need any more this extended functionality, you can drop it and return to the regular map:
studentMap = new HashMap<>(studentMap);
See also: The 'contains' method does not work for ArrayList<int[]>, is there another way?
Upvotes: 0