Java HashMap nested generics with wildcards
Solution 1:
Multi-level wildcards can be a bit tricky at times, when not dealt with properly. You should first learn how to read a multi-level wildcards. Then you would need to learn to interpret the meaning of extends
and super
bounds in multi-level wildcards. Those are important concepts that you must first learn before starting to use them, else you might very soon go mad.
Interpreting a multi-level wildcard:
**Multi-level wildcards* should be read top-down. First read the outermost type. If that is yet again a paramaterized type, go deep inside the type of that parameterized type. The understanding of the meaning of concrete parameterized type and wildcard parameterized type plays a key role in understand how to use them. For example:
List<? extends Number> list; // this is wildcard parameterized type
List<Number> list2; // this is concrete parameterized type of non-generic type
List<List<? extends Number>> list3; // this is *concrete paramterized type* of a *wildcard parameterized type*.
List<? extends List<Number>> list4; // this is *wildcard parameterized type*
First 2 are pretty clear.
Take a look at the 3rd one. How would you interpret that declaration? Just think, what type of elements can go inside that list. All the elements that are capture-convertible to List<? extends Number>
, can go inside the outer list:
-
List<Number>
- Yes -
List<Integer>
- Yes -
List<Double>
- Yes -
List<String>
- NO
References:
- JLS §5.1.10 - Capture Conversion
-
Java Generics FAQs - Angelika Langer
- Wildcard Capture
- IBM Developer Works Article - Understanding Wildcard Captures
Given that the 3rd instantiation of list can hold the above mentioned type of element, it would be wrong to assign the reference to a list like this:
List<List<? extends Number>> list = new ArrayList<List<Integer>>(); // Wrong
The above assignment should not work, else you might then do something like this:
list.add(new ArrayList<Float>()); // You can add an `ArrayList<Float>` right?
So, what happened? You just added an ArrayList<Float>
to a collection, which was supposed to hold a List<Integer>
only. That will certainly give you trouble at runtime. That is why it's not allowed, and compiler prevents this at compile time only.
However, consider the 4th instantiation of multi-level wildcard. That list represents a family of all instantiation of List
with type parameters that are subclass of List<Number>
. So, following assignments are valid for such lists:
list4 = new ArrayList<Integer>();
list4 = new ArrayList<Double>();
References:
- What do multi-level wildcards mean?
- Difference between a
Collection<Pair<String,Object>>
, aCollection<Pair<String,?>>
and aCollection<? extends Pair<String,?>>
?
Relating to single-level wildcard:
Now this might be making a clear picture in your mind, which relates back to the invariance of generics. A List<Number>
is not a List<Double>
, although Number
is superclass of Double
. Similarly, a List<List<? extends Number>>
is not a List<List<Integer>>
even though the List<? extends Number>
is a superclass of List<Integer>
.
Coming to the concrete problem:
You have declared your map as:
HashMap<String, Hashmap<String, HashSet<? extends AttackCard>>> superMap;
Note that there is 3-level of nesting in that declaration. Be careful. It's similar to List<List<List<? extends Number>>>
, which is different from List<List<? extends Number>>
.
Now what all element type you can add to the superMap
? Surely, you can't add a HashMap<String, HashSet<Assassin>>
into the superMap
. Why? Because we can't do something like this:
HashMap<String, HashSet<? extends AttackCard>> map = new HashMap<String, HashSet<Assassin>>(); // This isn't valid
You can only assign a HashMap<String, HashSet<? extends AttackCard>>
to map
and thus only put that type of map as value in superMap
.
Option 1:
So, one option is to modify your last part of the code in Assassin
class(I guess it is) to:
private HashMap<String, HashSet<? extends AttackCard>> pool = new HashMap<>();
public HashMap<String, HashSet<? extends AttackCard>> get() {
return pool;
}
... and all will work fine.
Option 2:
Another option is to change the declaration of superMap
to:
private HashMap<String, HashMap<String, ? extends HashSet<? extends AttackCard>>> superMap = new HashMap<>();
Now, you would be able to put a HashMap<String, HashSet<Assassin>>
to the superMap
. How? Think of it. HashMap<String, HashSet<Assassin>>
is capture-convertible to HashMap<String, ? extends HashSet<? extends AttackCard>>
. Right? So the following assignment for the inner map is valid:
HashMap<String, ? extends HashSet<? extends AttackCard>> map = new HashMap<String, HashSet<Assassin>>();
And hence you can put a HashMap<String, HashSet<Assassin>>
in the above declared superMap
. And then your original method in Assassin
class would work fine.
Bonus Point:
After solving the current issue, you should also consider to change all the concrete class type reference to their respective super interfaces. You should change the declaration of superMap
to:
Map<String, Map<String, ? extends Set<? extends AttackCard>>> superMap;
So that you can assign either HashMap
or TreeMap
or LinkedHashMap
, anytype to the superMap
. Also, you would be able to add a HashMap
or TreeMap
as values of the superMap
. It's really important to understand the usage of Liskov Substitution Principle.
Solution 2:
Don't use HashSet<? extends AttackCard>
, just use HashSet<AttackCard>
in all declarations - the superMap and all Sets being added.
You can still store subclasses of AttackCard
in a Set<AttackCard>
.
You should be declaring your variables using the abstract type, not the concrete implantation, ie:
Map<String, Map<String, Set<? extends AttackCard>>> superMap
See Liskov substitution principle