Eclipse is giving me a warning of the following form:
Type safety: Unchecked cast from Object to HashMap
This is from a call to
Almost every problem in Computer Science can be solved by adding a level of indirection*, or something.
So introduce a non-generic object that is of a higher-level that a Map
. With no context it isn't going to look very convincing, but anyway:
public final class Items implements java.io.Serializable {
private static final long serialVersionUID = 1L;
private Map<String,String> map;
public Items(Map<String,String> map) {
this.map = New.immutableMap(map);
}
public Map<String,String> getMap() {
return map;
}
@Override public String toString() {
return map.toString();
}
}
public final class New {
public static <K,V> Map<K,V> immutableMap(
Map<? extends K, ? extends V> original
) {
// ... optimise as you wish...
return Collections.unmodifiableMap(
new HashMap<String,String>(original)
);
}
}
static Map<String, String> getItems(HttpSession session) {
Items items = (Items)
session.getAttribute("attributeKey");
return items.getMap();
}
*Except too many levels of indirection.
Wow; I think I figured out the answer to my own question. I'm just not sure it's worth it! :)
The problem is the cast isn't checked. So, you have to check it yourself. You can't just check a parameterized type with instanceof, because the parameterized type information is unavailable at runtime, having been erased at compile time.
But, you can perform a check on each and every item in the hash, with instanceof, and in doing so, you can construct a new hash that is type-safe. And you won't provoke any warnings.
Thanks to mmyers and Esko Luontola, I've parameterized the code I originally wrote here, so it can be wrapped up in a utility class somewhere and used for any parameterized HashMap. If you want to understand it better and aren't very familiar with generics, I encourage viewing the edit history of this answer.
public static <K, V> HashMap<K, V> castHash(HashMap input,
Class<K> keyClass,
Class<V> valueClass) {
HashMap<K, V> output = new HashMap<K, V>();
if (input == null)
return output;
for (Object key: input.keySet().toArray()) {
if ((key == null) || (keyClass.isAssignableFrom(key.getClass()))) {
Object value = input.get(key);
if ((value == null) || (valueClass.isAssignableFrom(value.getClass()))) {
K k = keyClass.cast(key);
V v = valueClass.cast(value);
output.put(k, v);
} else {
throw new AssertionError(
"Cannot cast to HashMap<"+ keyClass.getSimpleName()
+", "+ valueClass.getSimpleName() +">"
+", value "+ value +" is not a "+ valueClass.getSimpleName()
);
}
} else {
throw new AssertionError(
"Cannot cast to HashMap<"+ keyClass.getSimpleName()
+", "+ valueClass.getSimpleName() +">"
+", key "+ key +" is not a " + keyClass.getSimpleName()
);
}
}
return output;
}
That's a lot of work, possibly for very little reward... I'm not sure if I'll use it or not. I'd appreciate any comments as to whether people think it's worth it or not. Also, I'd appreciate improvement suggestions: is there something better I can do besides throw AssertionErrors? Is there something better I could throw? Should I make it a checked Exception?
If you are sure that the type returned by session.getAttribute() is HashMap then you can not typecast to that exact type, but rely on only checking the generic HashMap
HashMap<?,?> getItems(javax.servlet.http.HttpSession session) {
HashMap<?,?> theHash = (HashMap<?,?>)session.getAttribute("attributeKey");
return theHash;
}
Eclipse will then surprise warnings, but of course this can lead to runtime errors that can be hard to debug. I use this approach in not operation-critical contexts only.
In the HTTP Session world you can't really avoid the cast, since the API is written that way (takes and returns only Object
).
With a little bit of work you can easily avoid the unchecked cast, 'though. This means that it will turn into a traditional cast giving a ClassCastException
right there in the event of an error). An unchecked exception could turn into a CCE
at any point later on instead of the point of the cast (that's the reason why it's a separate warning).
Replace the HashMap with a dedicated class:
import java.util.AbstractMap;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class Attributes extends AbstractMap<String, String> {
final Map<String, String> content = new HashMap<String, String>();
@Override
public Set<Map.Entry<String, String>> entrySet() {
return content.entrySet();
}
@Override
public Set<String> keySet() {
return content.keySet();
}
@Override
public Collection<String> values() {
return content.values();
}
@Override
public String put(final String key, final String value) {
return content.put(key, value);
}
}
Then cast to that class instead of Map<String,String>
and everything will be checked at the exact place where you write your code. No unexpected ClassCastExceptions
later on.
Solution: Disable this warning in Eclipse. Don't @SuppressWarnings it, just disable it completely.
Several of the "solutions" presented above are way out of line, making code unreadable for the sake of suppressing a silly warning.