Java HashMap nested generics with wildcards

2019-01-07 18:16发布

问题:

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)

回答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>>, 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.



回答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



回答3:

Probably a question of covariance, you need to replace ? extends by ? super.

See What is PECS (Producer Extends Consumer Super)?