raghuveer
raghuveer

Reputation: 113

How to combine Multiple Maps with multiple same keys and lists as values?

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

Answers (3)

WJS
WJS

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

dreamcrash
dreamcrash

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

user14838237
user14838237

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

Related Questions