8

Consider my custom extended hashmap:

public class CustomHashMap extends HashMap<String, Object> {
...
}

Why doesn't this work since CustomHashMap is child of HashMap?

Map<String, HashMap<String, Object>> customs = new LinkedHashMap<String, CustomHashMap>();

But this works:

Map<String, HashMap<String, Object>> customs = new LinkedHashMap();

And also it works when adding (put) an CustomHashMap into the customs Map.

customs.put("test", new CustomHashMap());

It seems weird that not specifying the generics at initialization works, but it doesn't otherwise.

user1236048
  • 5,542
  • 7
  • 50
  • 87
  • Did you try `new LinkedHashMap<>();`? –  Nov 14 '13 at 11:36
  • @LutzHorn - it doesn't -- compile error. Also the question is not to find a solution.. I can force the type myself. I'm just curious why it doesn't work using the obvious way (imo) – user1236048 Nov 14 '13 at 11:38
  • `<>` works, as does `LinkedHashMap customs = new LinkedHashMap<>();` –  Nov 14 '13 at 11:40
  • @LutzHorn - you're right. I need to change to JRE1.7 but it works – user1236048 Nov 14 '13 at 11:42
  • Java 7 is out for some years. Use it. –  Nov 14 '13 at 11:43
  • 2
    possible duplicate of [Is List a subclass of List? Why aren't Java's generics implicitly polymorphic?](http://stackoverflow.com/questions/2745265/is-listdog-a-subclass-of-listanimal-why-arent-javas-generics-implicitly-p) – Paul Bellora Nov 14 '13 at 15:36

3 Answers3

7

This statement is not working

Map<String, HashMap<String, Object>> customs = new LinkedHashMap<String, CustomHashMap>();

because customs is of type Map<String, HashMap<String, Object>> and you are assigning a LinkedHashMap which is of type <String, CustomHashMap>, where CustomHashMap is a sub class of HashMap<String, Object>.

Generics are invariant: for any two distinct types T1 and T2, HashMap<String, T1> is neither a subtype nor a supertype of HashMap<String, T2>. So, LinkedHashMap<String, CustomHashMap> cannot be assigned to Map<String, HashMap<String, Object>>. On the other hand, arrays are covariant, which means below statement will compile without any error or warning. But, it might fail at run time (which might cause more harm) if you put any other subtype of HashMap<String, Object> into it other than CustomHashMap :

HashMap<String, Object>[] mapArray = new CustomHashMap[1];
mapArray[0] = new CustomHashMap_1();// this will throw java.lang.ArrayStoreException

Now, if you want to assign LinkedHashMap<String, CustomHashMap> to Map<String, HashMap<String, Object>> , change the statement to this:

Map<String, ? extends HashMap<String, Object>> customs = new LinkedHashMap<String, CustomHashMap>();

Some additional information about this approach is nicely explained by @Seelenvirtuose , which is the accepted answer.

Debojit Saikia
  • 10,532
  • 3
  • 35
  • 46
  • it works at initialization.. but shouldn't it also work when adding an CustomHashMap value: `customs.put("test", new MessageHashMap());` -- compile cast error – user1236048 Nov 14 '13 at 12:05
  • also this doesn't work on your solution: `customs.put("test", new HashMap());` same compile -- cannot apply to arguments – user1236048 Nov 14 '13 at 12:08
  • 1
    When you need to put values into that map, you cannot use the upper-bounded wilcard when declaringthe variable. See my answer for the reason. – Seelenvirtuose Nov 14 '13 at 12:08
  • @Seelenvirtuose agree. liked your answer. great. – Debojit Saikia Nov 14 '13 at 12:16
  • 1
    "The type of the value of `customs` map can only hold values of type `HashMap` but not any of its subtypes". This makes it sound like `customs` can't contain a value of type `CustomHashMap`, which it can. It's that the type `LinkedHashMap` isn't assignable to `Map>` because generics aren't covariant. – Paul Bellora Nov 14 '13 at 15:48
  • @PaulBellora Yes, that was a wrong statement. Thanks for your comment. I have edited my answer. – Debojit Saikia Nov 15 '13 at 08:14
4

When working with generics, you should always keep type erasure in mind. At runtime an objct of type Map does not know its type parameters anymore. The consequence: A LinkedHashMap<String, CustomHashMap> is not a sub-type of Map<String, HashMap<String, Object>>.

If you want to have somthing sub-type related you must do it the following way:

Map<String, ? extends HashMap<String, Object>> customs = new LinkedHashMap<String, CustomHashMap>();

This is called an upper-bounded wildcard and exists exactly for that case: To get a sub-type relationship. Please refer to the Java tutorial about generics for more information.


An additional info as per the comment:

The upper-bounded version has a disadvantage on how to use the customs map. You cannot put instances anymore into that map. The only value allowed is null. The reason is, that you could have another class extending Map<String, HashMap> and try to put an instance of that into your customs map. But this is a problem, as the variable customs refers to a map that was parameterized with CustomHashMap.

When working with bounded wildcards, you should always remind PECS. PECS stands for "producer extends, consumer super". This is valuable for method parameters. If you write a method that only needs to read values from such a map, you could type the parameter as Map<String, ? extends Map<String, Object>>. This is called a producer. If you only need to write to that map, use the keyword super. If you need both - read and write - you cannot do either.

Debojit Saikia
  • 10,532
  • 3
  • 35
  • 46
Seelenvirtuose
  • 20,273
  • 6
  • 37
  • 66
  • 2
    Bear in mind that with that declaration, the only value you can add to "customs" is null, which is the only value assured to satisfy whatever value type is used in the map. – Glenn Lane Nov 14 '13 at 11:58
  • @GlennLane: You are right, I edited my answer (although that would also be explained in the linked tutorial). – Seelenvirtuose Nov 14 '13 at 12:07
  • Thank you. the answer is almost perfect.. Just for curiosity, is there any place in practice where this is used, since is only accepts `null`. It seems like a core bug – user1236048 Nov 14 '13 at 12:11
  • 1
    No, no, no. This is not a bug. This is again due to type erasure. A 'null' is the only value that fits for all types. Please, read the linked tutorial for more information. And as per the "place in practice", read about PECS. I already gave you a hint: method parameters. – Seelenvirtuose Nov 14 '13 at 12:12
  • 1
    Generic types can give an interface maximum flexibility... you could have an interface with method Map> getTheMap(), but your implementation can have signature Map getTheMap() and still fulfill the interface. – Glenn Lane Nov 14 '13 at 12:23
  • Exactly, it is for type flexibility. But it should be used only for the incoming parameters. The return parameter should not be bounded for the same reason you encountered. – Seelenvirtuose Nov 14 '13 at 12:26
  • 2
    -1 Type erasure is unrelated to why generics aren't covariant. – Paul Bellora Nov 14 '13 at 15:38
  • 1
    @PaulBellora: This is nonsense. Type erasure is exactly the reason for that behavior! Array types for example are not erased. What follows is the conclusion: If S is a sub-type of T, then S[] is also a sub-type of T[]. The same cannot be done with generics: A List is not a sub-type of List, the compiler enforces that. And guess what? This is due to type erasure, as at runtime there will be no way to detect a wrong adding. In arrays you can detect it and this then throws an appropriate exception. – Seelenvirtuose Nov 14 '13 at 16:05
  • I've had this debate with many people, and the problem is that their logic comes from the wrong direction. Array covariance is generally regarded as a mistake in retrospect, because it can lead to runtime errors that otherwise could be caught at compile time. The reason it was allowed was precisely because generics weren't available yet. See my answer [here](http://stackoverflow.com/questions/18666710/why-are-arrays-covariant-but-generics-are-invariant/18666878#18666878) for more info. – Paul Bellora Nov 14 '13 at 16:20
  • i believe @PaulBellora is referring about the reason java developers chose to make collections invariant and rather not about the technical reasons behind it. – Thirumalai Parthasarathi Nov 14 '13 at 16:21
  • @PaulBellora : i agree. but the technical explanation is not wrong is it..?? – Thirumalai Parthasarathi Nov 14 '13 at 16:22
  • We should be more precise in the wording. Array covariance is considered a mistake. So the Java designers made generics incovariant, without caring about type erasure (you could have incovariance without erasure). Additional, the incovariance was never the motivation for type erasure. The motivation was to simply have legacy code compatible. The raw type also exists because of that. But as we have type erasure, you simply could not decide to make generics covariant. In a technical sense, it is just not possible. Conclusion: The (technical) reason for incovariant behavior is type erasure. Ok? – Seelenvirtuose Nov 15 '13 at 07:11
2

From the java tutorial on oracle's site

List<String> ls = new ArrayList<String>(); // 1
List<Object> lo = ls; // 2 

Line 1 is certainly legal. The trickier part of the question is line 2. This boils down to the question: is a List of String a List of Object. Most people instinctively answer, "Sure!"

Well, take a look at the next few lines:

lo.add(new Object()); // 3
String s = ls.get(0); // 4: Attempts to assign an Object to a String!

Here we've aliased ls and lo. Accessing ls, a list of String, through the alias lo, we can insert arbitrary objects into it. As a result ls does not hold just Strings anymore, and when we try and get something out of it, we get a rude surprise.

The Java compiler will prevent this from happening of course. Line 2 will cause a compile time error.

this link would help you to learn generics and subtyping

Thirumalai Parthasarathi
  • 4,541
  • 1
  • 25
  • 43
  • 1
    -1 Type erasure is unrelated to why generics aren't covariant. – Paul Bellora Nov 14 '13 at 15:42
  • @PaulBellora : but [**this link**](http://docs.oracle.com/javase/tutorial/java/generics/restrictions.html#cannotCast) states that the cast is invalid because of type erasure. but i could have mis-interpreted it. Please tell me where i have gone wrong so that i can get the right thing. thx for pointing out..:) – Thirumalai Parthasarathi Nov 14 '13 at 15:52
  • 1
    That link is correct, but it only cites type erasure as the reason for a different limitation - that you can't use `instanceof` to check for a specific generic type, e.g. `instanceof ArrayList`. Admittedly it doesn't do a good job of specifically explaining why a `List` isn't a `List` but I think that's a tangential example leading into the fact that you can cast from e.g. `List` to `ArrayList`. – Paul Bellora Nov 14 '13 at 16:29
  • I just went through generics and subtyping in the java doc tutorial and understood what you were trying to say. i should probably edit my answer or delete it. im thinking of which one to do.. :) – Thirumalai Parthasarathi Nov 14 '13 at 16:33