I'm trying to make a hashmap of hashmap values containing hashsets of different subclasses of a custom class, like so:
HashMap<String, Hashmap<String, HashSet<? extends AttackCard>>> superMap
AttackCard has subclasses such as: Mage, Assassin, Fighter. Each HashMap in the superMap will only ever have HashSets containing a single subclass of AttackCard.
When I try putting a
HashMap<String, HashSet<Assassin>>
into superMap, I get a compiler error:

below is the code where the error occurs:
public class CardPool {
private HashMap<String, HashMap<String, HashSet<? extends AttackCard>>> attackPool =
new HashMap<>();
private ArrayList<AuxiliaryCard> auxiliaryPool;
public CardPool() {
(line 24)this.attackPool.put("assassins", new AssassinPool().get());
/* this.attackPool.put("fighters", new Fighter().getPool());
this.attackPool.put("mages", new Mage().getPool());
this.attackPool.put("marksmen", new Marksman().getPool());
this.attackPool.put("supports", new Support().getPool());
this.attackPool.put("tanks", new Tank().getPool());
*/
this.auxiliaryPool = new ArrayList<>(new AuxiliaryCard().getPool());
}
And here is a snippet of the AssassinPool get-method:
private HashMap<String, HashSet<Assassin>> pool = new HashMap<>();
public HashMap<String, HashSet<Assassin>> get() {
return pool;
}
I'd like to comment that I could easily solve my problem and have a wonderfully working program by making all the AttackCardPools, such as AssassinPool, return and contain HashSets of AttackCard instead of their respective subclass. I'm trying to understand this compilation error, however :)
compilation error at line 24: error: no suitable method found for `put(String, HashMap<String,HashSet<Assassin>>>`
this.attackPool.put("assassins", new AssassinPool(). get());
method HashMap.putp.(String, HashMap<String,HashSet<? extends AttackCard>>>` is not applicable (actual argument `HashMap<String, HashSet<Assassin>>` cannot be converted to `HashMap<String, HashSet<? extends AttackCard>>` by method invocation conversion)
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> - YesList<Integer> - YesList<Double> - YesList<String> - NO
References:
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:
Collection<Pair<String,Object>>, a Collection<Pair<String,?>> and a Collection<? 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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With