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>
- Yes
List<Integer>
- Yes
List<Double>
- Yes
List<String>
- NO
References:
- JLS §5.1.10 - Capture Conversion
- Java Generics FAQs - Angelika Langer
- 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>>
, 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