14

I'm trying to understand why this code has an unchecked cast warning. The first two casts have no warning, but the third does:

class StringMap<V> extends HashMap<String, V> {
}

class StringToIntegerMap extends HashMap<String, Integer> {
}

Map<?, ?> map1 = new StringToIntegerMap();
if (map1 instanceof StringToIntegerMap) {
    StringToIntegerMap stringMap1 = (StringToIntegerMap)map1; //no unchecked cast warning
}

Map<String, Integer> map2 = new StringMap<>();
if (map2 instanceof StringMap) {
    StringMap<Integer> stringMap2 = (StringMap<Integer>)map2; //no unchecked cast warning
}

Map<?, Integer> map3 = new StringMap<>();
if (map3 instanceof StringMap) {
    StringMap<Integer> stringMap3 = (StringMap<Integer>)map3; //unchecked cast warning
}

This is the full warning for the stringMap3 cast:

Type safety: Unchecked cast from Map<capture#3-of ?,Integer> to StringMap<Integer>

However, the StringMap class declaration specifies the first type parameter of Map (i.e., String), and both map3 and the StringMap<Integer> cast use the same type for the second type parameter of Map (i.e., Integer). From what I understand, as long as the cast doesn't throw ClassCastException (and it shouldn't since there is an instanceof check), stringMap3 would be a valid Map<String, Integer>.

Is this a limitation of the Java compiler? Or is there a scenario where calling methods of either map3 or stringMap3 with certain arguments may result in an unexpected ClassCastException if the warning is ignored?

blurredd
  • 185
  • 8

4 Answers4

8

The behavior is as specified. In Section 5.5.2 of the Java Language Specification an unchecked cast is defined as:

A cast from a type S to a parameterized type T is unchecked unless at least one of the following is true:

  • S <: T

  • All of the type arguments of T are unbounded wildcards

  • T <: S and S has no subtype X other than T where the type arguments of X are not contained in the type arguments of T.

(where A <: B means: "A is a subtype of B").

In your first example, the target type has no wildcards (and thus all of them are unbounded). In your second example, StringMap<Integer> is actually a subtype of Map<String, Integer> (and there is no subtype X as mentioned in the third condition).

In your third example, however, you have a cast from Map<?, Integer> to StringMap<Integer>, and, because of the wildcard ?, neither is a subtype of the other. Also, obviously, not all type parameters are unbounded wildcards, so none of the conditions apply: it is an unchecked exception.

If an unchecked cast occurs in the code, a conforming Java compiler is required to issue a warning.

Like you, I do not see any scenario where the cast would be invalid, so you could argue that it is a limitation of the Java compiler, but at least it is a specified limitation.

Hoopje
  • 12,677
  • 8
  • 34
  • 50
  • This was the conclusion I was coming to after reading @pifta's answer and various sections in the JLS. I suppose the JLS could be updated to add better casting rules between two parameterized types where the respective generic classes use a different number of type parameters, but I wouldn't know how feasible this is or the side effects. This question appears to be related, unless I'm mistaken: [Casting to generic subtypes of a generic class](//stackoverflow.com/questions/17883536/casting-to-generic-subtypes-of-a-generic-class). – blurredd Dec 03 '15 at 17:42
3

This cast isn't safe. Let's say you have:

Map<?, Integer> map3 = new HashMap<String,Integer>();
StringMap<Integer> stringMap3 = (StringMap<Integer>)map3;

That's going to throw an exception. It doesn't matter that you know you newed up a StringMap<Integer> and assigned it to the map3. What you're doing is known as down-casting Downcasting in Java for more info.

EDIT: You're also over complicating the problem with all the generics, you will have the exact same issue without any generic types.

Community
  • 1
  • 1
Charles Durham
  • 2,445
  • 16
  • 17
  • Exactly. Essentially, you're trying to cast a "Map, Integer>" into a "HashMap", so a ? into a String... – JayC667 Nov 23 '15 at 01:11
  • The use of generics is central to the question. By "safe", I meant whether it was possible to call a method (or a set of methods) with certain arguments for either map3 or stringMap3 that would cause an unexpected ClassCastException if the warning was ignored. I wasn't asking if the initial cast to StringMap might cause a ClassCastException. I updated the question. – blurredd Nov 23 '15 at 01:17
3

Actually the answer is in the Java Language Specification. Section 5.1.10 mentions that if you use a wildcard, then it will be a fresh capture type.

This implies that, Map<String, Integer> is not a subclass of Map<?, Integer>, and therefore even if the StringMap<Integer> is assignable to a Map<?, Integer> type, because Map<?, Integer> represents a map with some key type and Integer values, which is true to StringMap<Integer>, the cast is unsafe. So this is why you get an unchecked cast warning.

From the compiler point of view between the assignment, and the cast there could be anything, so even if map3 is an instance of StringMap<Integer> at the cast operation, map3 has Map<?, Integer> as its type, so the warning is completely legit.

And to answer for your question: yes, until the map3 instance has only strings as keys, and your code could not possibly guarantee that.

See why not:

Map<?, Integer> map3 = new StringMap<Integer>();

// valid operation to use null as a key. Possible NPE depending on the map implementation
// you choose as superclass of the StringMap<V>.
// if you do not cast map3, you can use only null as the key.
map3.put(null, 2); 

// but after an other unchecked cast if I do not want to create an other storage, I can use
// any key type in the same map like it would be a raw type.
((Map<Double, Integer>) map3).put(Double.valueOf(0.3), 2); 

// map3 is still an instance of StringMap
if (map3 instanceof StringMap) {
    StringMap<Integer> stringMap3 = (StringMap<Integer>) map3; // unchecked cast warning
    stringMap3.put("foo", 0);
    for (String s : stringMap3.keySet()){
        System.out.println(s+":"+map3.get(s));
    }
}

The result:

null:2
foo:0
Exception in thread "main" java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.String
pifta
  • 186
  • 6
  • Oh, and by the way, one can abuse the first version this way also, that needs an other unchecked cast. And until you do that unchecked cast, you can only use the put method of the map with null as the key, and null as the value, which is safe in a way, because if the map implementation supports null keys and null values, then your read operations should consider the possible null key and null value in the map. – pifta Nov 30 '15 at 19:06
  • I didn't mean to imply there would be other unchecked casts other than the one to `StringMap`. You can potentially corrupt any map--or any generic object for that matter--with the right unchecked cast, so your example doesn't answer whether casting from `Map, Integer>` to `StringMap` could ever allow heap pollution unlike, say, casting from `Map, Integer>` to `Map`. You can create a scenario where the map assigned to `map3` is populated elsewhere or uses a different map implementation if that helps. – blurredd Dec 02 '15 at 18:12
  • Ok, after your comment I found the same answer that was given by Hoopje just a few minutes ago, I overlooked that part of the jls before. Anyways, it is an unchecked cast based on 5.5.2. However I still think that it is not a good practice to use this kind of casts. – pifta Dec 02 '15 at 23:13
3

Of course, it is not possible to provide an answer which is "more correct" (correctier?) than one which explains the observed behaviour in terms of the Java Language Specification. However:

  • An explanation of the observed behaviour given in practical terms can be easier to follow and easier to remember than an explanation which just throws the JLS at you. As a result, a practical explanation is often more usable than an explanation in terms of the JLS.

  • The JLS is precisely the way it is and not in any other way, because it needs to satisfy practical constraints. Given the fundamental choices made by the language, quite often the details of the JLS could not have been any other way than the way it is. This means that the practical reasons for a specific behaviour can be thought of as more important than the JLS, because they shaped the JLS, the JLS did not shape them.

So, a practical explanation of what is happening follows.


The following:

Map<?, ?> map1 = ...;
StringToIntegerMap stringMap1 = (StringToIntegerMap)map1; 

gives no unchecked cast warning because you are not casting to a generic type. It is the same as doing the following:

Map map4 = ...; //gives warning "raw use of generic type"; bear with me.
StringToIntegerMap stringMap4 = (StringToIntegerMap)map4; //no unchecked warning!

The following:

Map<String, Integer> map2 = ...;
StringMap<Integer> stringMap2 = (StringMap<Integer>)map2;

gives no unchecked cast warning because generic arguments of left hand side match generic arguments of right hand side. (Both are <String,Integer>)


The following:

Map<?, Integer> map3 = ...;
StringMap<Integer> stringMap3 = (StringMap<Integer>)map3;

does give an unchecked cast warning because left hand side is <String,Integer> but right hand side is <?,Integer> and you can always expect such a warning when you cast a wildcard to specific type. (Or a bounded type to a more strictly bounded, more specific type.) Note that "Integer" in this case is a red herring, you would get the same thing with Map<?,?> map3 = new HashMap<>();

Mike Nakis
  • 56,297
  • 11
  • 110
  • 142
  • But the cast is actually not from `Map,Integer>` to `Map` (which most answers address), but from `Map,Integer>` to `StringMap`. This makes the question much more interesting, because if an object is a `StringMap` (a check which can be performed at runtime), then it must be a `Map`. So, intuitively, the cast is not unchecked at all. – Hoopje Dec 05 '15 at 20:18