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)
denvercoder9
  • 2,979
  • 3
  • 28
  • 41
user2651804
  • 1,464
  • 4
  • 22
  • 45
  • 1
    Post the exception stack trace here as text and not an image or a link to an image. – Rahul Oct 07 '13 at 08: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. – sp00m Oct 07 '13 at 08:07
  • compilation error at line 24: error: no suitable method found for put(String, HashMap>> this.attackPool.put("assassins", new AssassinPool(). get()); method HashMap.putp.(String, HashMap>> is not applicable (actual argument HashMap> cannot be converted to HashMap> by method invocation conversion) – user2651804 Oct 07 '13 at 08:19
  • 2
    don't post images on SO, unless it is really an image. Write the error message in text. – Rohit Jain Oct 07 '13 at 08:20

3 Answers3

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.

Rohit Jain
  • 209,639
  • 45
  • 409
  • 525
  • 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()) – user2651804 Oct 07 '13 at 09:25
  • 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 – user2651804 Oct 07 '13 at 09:38
  • While my field in AssassinPool is HashMap> I can't return it as HashMap> :( I am depressingly confused – user2651804 Oct 07 '13 at 09:46
  • @user2651804 For the same reason you can't assign a `List>` to a `List>`. I've done some modification in my answer. Please take a look again. Earlier it was a bit messed out. – Rohit Jain Oct 07 '13 at 09:48
  • 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. But here in the comments you say that List doesn't go into list 3 either? – user2651804 Oct 07 '13 at 10:18
  • @user2651804 You mixing both the statements. They mean different thing. There I said, you can add a `List` to `List>`. Here I'm saying that, you can't assign a `List>` to a `List>`. Both have different meanings. – Rohit Jain Oct 07 '13 at 10:21
  • Which leads to the simple question: what does extends SomeSuperclass> do?? (if it doesn't accept subclasses, that is) I will try to read some generics tutorials now – user2651804 Oct 07 '13 at 10:22
  • @user2651804 I guess yes. You should go through some tutorial, to grab a better understanding of bounded wildcards. You can also go through [my other answer](http://stackoverflow.com/q/18474379/1679863) where I've explained in brief what they mean. – Rohit Jain Oct 07 '13 at 10:23
  • "you can add a List to List>." That's what I'm trying to do in my code?? it doesn't work! I'm trying to add HashMap> to HashMap>> basically?? Is there some miscommunication going on? To my eyes these examples are in complete sync!! – user2651804 Oct 07 '13 at 10:30
  • @user2651804 Well, you're not doing the same thing. If you notice in my answer I said - there is 3-level of nesting (Be Careful). That is what makes both the examples different. Since `List` is a subclass of `List extends Number>`, so you can add it to the given list. But again, a `HashMap>` is not a subclass of `HashMap>`. Although it's a subclass of `HashMap>`. Notice the position of `?`. – Rohit Jain Oct 07 '13 at 10:32
  • Yeah I see. I can't admit that I completely understand. I need some break/study time. A quick word along the way: couldn't my solution be as simple as changing the "? extends" declaration of superMap? – user2651804 Oct 07 '13 at 10:54
  • @user2651804 I've edited the answer to show how you can change the declaration of your `superMap` to allow your original method to work fine. Check the part of my answer below - **`Option 2`**. – Rohit Jain Oct 07 '13 at 11:01
  • Well, I haven't given it any thought yet, but it works like a charm. Wow, thank you so much for your time and everything! It's surprising that it took so long to get this question answered (I've asked several places, several times) Guess I'll have to study the answer and why it works later. And honestly I'm relieved I don't have to delve into generics, haha – user2651804 Oct 07 '13 at 11:20
  • @user2651804 haha :) You're welcome. But I would really suggest you to learn generics from basics. It is really an interesting topic, and very important one. You can start from http://docs.oracle.com/javase/tutorial/java/generics/ – Rohit Jain Oct 07 '13 at 11:23
  • +1 for an excellent answer. Unfortunately it was CW'd by so many edits. Try flagging a mod and asking to have it un-CW'd. – Paul Bellora Oct 07 '13 at 14:07
  • @PaulBellora Oh! Didn't notice I've edited it 11 times. Thanks :) – Rohit Jain Oct 07 '13 at 14:10
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

Bohemian
  • 412,405
  • 93
  • 575
  • 722
  • Although changing `HashSet extends AttackCard>` to `HashSet` will allow him to add subclasses in the set. But this will not allow to add a `HashSet` into that `map`. – Rohit Jain Oct 07 '13 at 08:56
  • @RohitJain I'm suggesting *all* declarations be `Set` everywhere. – Bohemian Oct 07 '13 at 09:25
  • @Bohemian Yeah fine. But I guess you the declaration should be - `Map>>`. But you can't put a `HashSet` in the inner map. That was what I was saying. – Rohit Jain Oct 07 '13 at 09:28
  • 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 I could add Assassin to key1 and Fighter to key2 without problems? – user2651804 Oct 07 '13 at 09:49
  • @user2651804 Yes you can. I would suggest you to go through some generics tutorial before starting using multi-level wildcards. – Rohit Jain Oct 07 '13 at 09:52
  • above comment was meant to be posted to Rohit Jain's answer =] – user2651804 Oct 07 '13 at 09:53
0

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

See What is PECS (Producer Extends Consumer Super)?

Community
  • 1
  • 1
Gab
  • 7,869
  • 4
  • 37
  • 68
  • After my failed attempt at understanding the thread, I just went ahead an replaced extends with super. I get the same compilation error =/ – user2651804 Oct 07 '13 at 08:10