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());
}
}