2

I am trying to better understand generics in Java and therefore wrote a generic method that merges two maps of collections. (Please ignore for the moment that it creates a hard-coded ArrayList.)

public static <E, K> void  mergeMaps(Map<K, Collection<E>> receivingMap, Map<K, Collection<E>> givingMap) {
    for (Map.Entry<K, Collection<E>> entry : givingMap.entrySet()) {
        Collection<E> someCollection = receivingMap.computeIfAbsent(entry.getKey(), k -> new ArrayList<E>());
        someCollection.addAll(entry.getValue());
    }
}

My goal is that the mergeMaps function is able to merge maps (of the same type) whose values can be arbitrary collections (ArrayList,LinkedHashMap,...).

However, when I try to merge let's say two instances of Map<Integer, ArrayList<String>> I get a compile-time error but I do not quite understand what the compiler is telling me.

public static void main(String[] args) {
    Map<Integer, ArrayList<String>> map1 = new HashMap<>();
    Map<Integer, ArrayList<String>> map2 = new HashMap<>();
    mergeMaps(map1, map2); // <-- compile error
}

What is wrong here and how can I fix it?

Error:(9, 9) java: method mergeMaps in class CollectionUtil cannot be applied to given types;
  required: java.util.Map<K,java.util.Collection<E>>,java.util.Map<K,java.util.Collection<E>>
  found: java.util.Map<java.lang.Integer,java.util.ArrayList<java.lang.String>>,java.util.Map<java.lang.Integer,java.util.ArrayList<java.lang.String>>
  reason: cannot infer type-variable(s) E,K
    (argument mismatch; java.util.Map<java.lang.Integer,java.util.ArrayList<java.lang.String>> cannot be converted to java.util.Map<K,java.util.Collection<E>>)
Antimon
  • 241
  • 1
  • 10

2 Answers2

2

When the signature of the method is

<E, K> void  mergeMaps(Map<K, Collection<E>> receivingMap, 
                       Map<K, Collection<E>> givingMap)

Then a call using Map<Integer, List<String>> as argument types is invalid because Collection is not a generic type parameter of the mergeMaps method.
Why is this a problem? With generics, Map<Integer, List<String>> cannot be assigned to a Map<Integer, Collection<String>> variable (or passed as a method argument in that manner). This is because generic types are invariant (see here for more info. In short, that means List<Integer> is not necessarily compatible with any List<Number>, although a ArrayList<Number> is compatible with List<Number> ).

In other words, the concrete arguments must be of type Map<Integer, Collection<String>>. This leads to your first solution:

//Solution 1: change your arguments to Map<Integer, Collection<String>>:
Map<Integer, Collection<String>> map1 = new HashMap<>();
Map<Integer, Collection<String>> map2 = new HashMap<>();
mergeMaps(map1, map2);

If you want to allow calls with parameters of type Map<Integer, List<String>>, then you have to change your target method to introduce a generic parameter around the map value:

public static <E, K, C extends Collection<E>> void 
             mergeMaps2(Map<K, C> receivingMap, Map<K, C> givingMap) {
    for (Map.Entry<K, C> entry : givingMap.entrySet()) {
        Collection<E> someCollection = receivingMap.computeIfAbsent(entry.getKey(), 
                                         k -> (C) new ArrayList<E>());
        someCollection.addAll(entry.getValue());
    }
}

And that can be called with maps where the value is declared as a subtype of Collection<E> (as long as the Collection type is the same in both arguments):

Map<Integer, List<String>> map1 = new HashMap<>();
Map<Integer, List<String>> map2 = new HashMap<>();
mergeMaps2(map1, map2);

Map<Integer, Set<String>> map1 = new HashMap<>();
Map<Integer, Set<String>> map2 = new HashMap<>();
mergeMaps2(map1, map2);

Side note (or digression)

Now, when you compile this, you have a further problem: there's a compiler warning on this line:

Collection<E> someCollection = 
    receivingMap.computeIfAbsent(entry.getKey(), k -> (C) new ArrayList<E>());

Claiming that (C) new ArrayList<E>() is an unchecked cast. Why this? Let's look at the above example calls (I added the two advisedly):

Call 1:

Map<Integer, List<String>> map1 = new HashMap<>();
Map<Integer, List<String>> map2 = new HashMap<>();
mergeMaps2(map1, map2);

In this example, receivingMap.computeIfAbsent(entry.getKey(), k -> (C) new ArrayList<E>()) means to add an instance of ArrayList<String> as a value to the map. As the actual object is of a type that is compatible with the caller's declared type (List<String>), things are OK.

Now, what do you think this will do?

Call 2:

Map<Integer, Set<String>> map1 = new HashMap<>();
Map<Integer, Set<String>> map2 = new HashMap<>();
mergeMaps2(map1, map2);

In this case too, unfortunately, receivingMap.computeIfAbsent(entry.getKey(), k -> (C) new ArrayList<E>()) will still try to add an ArrayList<String>, which happens to be incompatible with the caller's expected value type (Set<String>).

The compiler can't be sure that the cast (C) new ArrayList<E>() will always be correct in the context of the concrete type arguments. It gives up, but issues a warning to alert the developer.

Dealing with this is actually a tricky problem. You need to know what type to instantiate, but your method parameters won't allow you to do so because you can't just run new C(). Your own requirements and design will determine the correct solution, but I'll end with one possible solution:

public static <E, K, C extends Collection<E>> void 
    mergeMaps2(Map<K, C> receivingMap, 
               Map<K, C> givingMap, 
               Supplier<C> collectionCreator) {
    for (Map.Entry<K, C> entry : givingMap.entrySet()) {
        Collection<E> someCollection = receivingMap.computeIfAbsent(entry.getKey(),
                        k -> collectionCreator.get());
        someCollection.addAll(entry.getValue());
    }
}
ernest_k
  • 44,416
  • 5
  • 53
  • 99
0

The error says java.util.List<java.lang.String>> cannot be converted to java.util.Map<K,java.util.Collection<E>>)

You have to modify your method:

public static <E, K> void  mergeMaps(Map<K, List<E>> receivingMap, Map<K, List<E>> givingMap) {
    for (Map.Entry<K, List<E>> entry : givingMap.entrySet()) {
        Collection<E> someCollection = receivingMap.computeIfAbsent(entry.getKey(), k -> new ArrayList<E>());
        someCollection.addAll(entry.getValue());
    }
}
Schidu Luca
  • 3,897
  • 1
  • 12
  • 27