2

I use this common initialization format when I anticipate changing the implementation of the List interface at a later time:

List<Foo> foos = new ArrayList<Foos>();

In an effort to gain the same utility for the values within a Map, I attempted the following but my compiler whines about List<> and ArrayList<> being incompatible types.

Map<String, List<Foo>> fooMap = new HashMap<String, ArrayList<Foo>>;

I've been unable to find an explanation for why I cannot initialize the map in this manner and I'd like to understand the reasoning.

And, sure, this works...

Map<String, List<Foo>> foosMap = new HashMap<String, List<Foo>>;
// ... populate map
ArrayList<Foo> foosAryLst = (ArrayList)foosMap.get("key1"); 

... but I'm a curious castaphobe. I'd rather fix compile-time errors than runtime errors, things like this aggravate my OCD and the smell of casting conjures an odor similar to the urinal trough after free deep-fried asparagus night at the stadium.

My questions come down to:

  1. Why can I not code my map values to an interface.
  2. Is there a workaround that doesn't require casting?

Any input will be appreciated, thanks!

IdusOrtus
  • 1,005
  • 1
  • 16
  • 24
  • 1
    `List list = new ArrayList();` won't compile. If you understand this, then you will understand why `Map> map = new HashMap>()` doesn't compile either. – Luiggi Mendoza Aug 29 '14 at 20:39
  • In your first example, the assigned class implements the declared interface, but it won't compile due to the generic types mismatch --> Interface != ClassImplementingInterface. So the implication is that with Map the generic type is specifically the interface, and it doesn't consider a class which implements the interface as a match with V? – IdusOrtus Aug 29 '14 at 21:18
  • Yes. And that's not only for `V` but for any generic. Note that the rule is: `SupportingGenerics foo = new SupportingGenerics()` won't compile. – Luiggi Mendoza Aug 29 '14 at 21:19
  • 1
    It's not an exact duplicate but highly related: [Is List a subclass of List? Why aren't Java's generics implicitly polymorphic?](http://stackoverflow.com/q/2745265/1065197) – Luiggi Mendoza Aug 29 '14 at 21:25
  • Much thanks! I never made that connection. I presumed the generic match/restriction was based on just the content type of the collection, not the collection itself. And, thanks for the rule defined in code - it helped me better understand List extends Object> Obj = new ArrayList(); – IdusOrtus Aug 29 '14 at 21:30
  • One more thing: You should not declare variables like `List extends YourClass>` for storage, the compiler won't allow adding elements to this kind of generics. – Luiggi Mendoza Aug 29 '14 at 21:34
  • I cannot .add() individual elements to such a list, but I can construct like List extends Superclass> = new ArrayList(existingSubclassList); Didn't know that either. Thanks again! – IdusOrtus Aug 29 '14 at 21:55

3 Answers3

4

Sure, there's a workaround that doesn't require casting: don't cast; write

List<Foo> foosLst = foosMap.get("key1"); 

...and code to the interface with the List as well as the Map.

The root issue, though, is that a Map<String, ArrayList<Foo>> isn't substitutable wherever you'd use Map<String, List<Foo>>. In particular,

Map<String, List<Foo>> map = new HashMap<>();
map.put("foo", new LinkedList<Foo>());

works, but not if map is a Map<String, ArrayList<Foo>>. So one isn't a drop-in substitute for the other.

Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
  • Perhaps mention wildcards? – Thorn G Aug 29 '14 at 20:38
  • @TomG if using wildcards, then you cannot add anything to the collection. – Luiggi Mendoza Aug 29 '14 at 20:39
  • Thanks for the response! That answers question #2, and illustrates that I can use any class implementing the List interface within the Map. I am still a bit unclear re: inability to do Map> fooMap = new HashMap>; Am I missing something that makes this type of initialization pointless want? – IdusOrtus Aug 29 '14 at 20:50
  • Marked as correct answer, but Luiggi's response provided excellent clarification for question #1. List list = new ArrayList(); won't compile. If you understand this, then you will understand why Map> map = new HashMap>() doesn't compile either. – Luiggi Mendoza – IdusOrtus Aug 29 '14 at 21:59
  • @IdusOrtus: That was what I was explaining, that a `HashMap>` cannot be used anywhere a `Map>` can be, so you can't assign it to a variable of that type. – Louis Wasserman Aug 29 '14 at 23:26
  • True, and thanks. Im sure others understood, but It didn't sink in for me until I connected the dots laid out. – IdusOrtus Aug 30 '14 at 00:36
1

The declaration that you proposed

Map<String, List<Foos>> fooMap = new HashMap<String, ArrayList<Foos>>();

simply does not make sense: The variable fooMap has the type Map<String, List<Foos>>. This means:

  • every value that you obtain from this map is a List<Foos>
  • you may put every value into this list that is (of a subtype of) List<Foos>

If you wanted a map that has ArrayLists as its values, then you would declare it as

Map<String, ArrayList<Foos>> fooMap = new HashMap<String, ArrayList<Foos>>();

If you don't care about the list type, then you can say

Map<String, List<Foos>> fooMap = new HashMap<String, List<Foos>>();

But there's no sensible meaning of mixing the two. Even if you could write what you proposed, then you could still not obtain an ArrayList from this map, because this is simply not the type that fooMap was declared with.


In most cases,

Map<String, List<Foos>> fooMap = new HashMap<String, List<Foos>>();

should be appropriate. Depending on the use case, one could possibly go further by saying

Map<String, List<? extends Foos>> fooMap = new HashMap<String, List<? extends Foos>>();

This way, you can also put lists into the map that contain sublcasses of Foos, like

List<SpecialFoos> specialFoos = ...
fooMap.put("special", specialFoos);

But of course, it's up to you to decide whether this is necessary or not.

Marco13
  • 53,703
  • 9
  • 80
  • 159
0

The core of the problem is that the compiler cannot keep track of what fooMap may have been assigned to at any particular point in the execution of your code, so there is no way for the compiler to know that

fooMap.put("abc", new ArrayList<Foo>())

should be legal, but that

fooMap.put("abc", new LinkedList<Foo>())

should not be.

All that the compiler knows about the typing of fooMap is its declared type Map<String, List<Foo>>. So, it enforces that whatever object to which you assign fooMap must be able to support all of the operations which a generic Map<String, List<Foo>> is capable of executing. The second line of code above is clearly legal for a Map<String, List<Foo>>, but not legal for a Map<String, ArrayList<Foo>>, so the compiler forbids you from assigning fooMap to a Map<String, ArrayList<Foo>>.

sumitsu
  • 1,481
  • 1
  • 16
  • 33