7

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: comiler 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)
4
  • 1
    Post the exception stack trace here as text and not an image or a link to an image. Commented Oct 7, 2013 at 8:03
  • 3
    a hashmap of hashmaps containing hashsets --> too deep, you should try to write a custom object that handles values of your top-map. Commented Oct 7, 2013 at 8:07
  • 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) Commented Oct 7, 2013 at 8:19
  • 2
    don't post images on SO, unless it is really an image. Write the error message in text. Commented Oct 7, 2013 at 8:20

3 Answers 3

16

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:

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:


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.

Sign up to request clarification or add additional context in comments.

16 Comments

I would like to comment that I'm initializing the superMap to be just "new Hashset<>()". The actual HashMaps contained in superMap come from other classes. like: superMap.put("key1", new OtherClass.getHashMap())
Also in the end the solution is a bit disappointing. Because the problem arose in the first place, because I wanted a strong type check in the respective subclasses. For instance AssassinPool should contain HashSet ONLY of Assassin
While my field in AssassinPool is HashMap<String, HashSet<Assassin>> I can't return it as HashMap<String, HashMap<? extends AttackCard>> :( I am depressingly confused
@user2651804 For the same reason you can't assign a List<List<Integer>> to a List<List<? extends Number>>. I've done some modification in my answer. Please take a look again. Earlier it was a bit messed out.
I've spend a long, long time starring blind at your answer. Have you maybe made a logical mistake? You're saying that the only thing that doesn't go into list3 is List<String>. But here in the comments you say that List<Integer> doesn't go into list 3 either?
|
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

6 Comments

Although changing HashSet<? extends AttackCard> to HashSet<AttackCard> will allow him to add subclasses in the set. But this will not allow to add a HashSet<Assassin> into that map.
@RohitJain I'm suggesting all declarations be Set<AttackCard> everywhere.
@Bohemian Yeah fine. But I guess you the declaration should be - Map<String, Map<String, Set<AttackCard>>>. But you can't put a HashSet<Assassin> in the inner map. That was what I was saying.
Okay, before I go diving deep into generic classes, could you confirm that I wouldn't have a problem at all if my map wasn't 2 levels? I mean if I wanted HashMap<String, AttackCard> I could add Assassin to key1 and Fighter to key2 without problems?
@user2651804 Yes you can. I would suggest you to go through some generics tutorial before starting using multi-level wildcards.
|
0

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

See What is PECS (Producer Extends Consumer Super)?

1 Comment

After my failed attempt at understanding the thread, I just went ahead an replaced extends with super. I get the same compilation error =/

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.