Reputation: 185
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>
toStringMap<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?
Upvotes: 14
Views: 2514
Reputation: 61969
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<>();
Upvotes: 3
Reputation: 12932
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 typeT
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
andS
has no subtypeX
other thanT
where the type arguments ofX
are not contained in the type arguments ofT
.
(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.
Upvotes: 8
Reputation: 186
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
Upvotes: 3
Reputation: 2495
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.
Upvotes: 3