Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoid unchecked assignment in a map with multiple value types?

I'm having trouble with a warning in Java 7:

Unchecked assignment: 'java.lang.Class' to 'java.lang.Class<T>' 

I'm getting it on the line Class<T> type = typeMap.get(key); in the get function below.

Basically what I'm trying to do here is I want to store a bunch of key/value pairs of unknown types (but all are descendants of Object with the exception of null), but not lose the type. So I created a class with the following content using generics. It has two maps (one to store the data and one to store the class type:

    private Map<String, Object>  dataMap = new HashMap<>();     private Map<String, Class> typeMap = new HashMap<>();      public  <T> void put(String key, T instance){         dataMap.put(key, instance);         if (instance == null){             typeMap.put(key,null);         }         else {             typeMap.put(key, instance.getClass());         }     }      public <T> T get(String key){         Class<T> type = typeMap.get(key);         if (type == null){             return null;         }         return type.cast(dataMap.get(key));     } 

It runs just fine, but the warning is annoying me. Is there any way to get Java to do this cast without complaining (other than suppressing it)? Or is there a better way to accomplish what I'm trying to do? How about in Java 8 as I haven't really had a chance to dive into it yet?

Thanks!

like image 428
irotsoma Avatar asked Mar 17 '14 23:03

irotsoma


People also ask

How can I avoid unchecked cast warnings?

If we can't eliminate the “unchecked cast” warning and we're sure that the code provoking the warning is typesafe, we can suppress the warning using the SuppressWarnings(“unchecked”) annotation. When we use the @SuppressWarning(“unchecked”) annotation, we should always put it on the smallest scope possible.

What does unchecked assignment mean in Java?

Unchecked cast means that you are (implicitly or explicitly) casting from a generic type to a nonqualified type or the other way around. E.g. this line. Set<String> set = new HashSet();

How do I cast an object to a map?

One solution is to use reflection and reflectively read the fields of the object and create the map from it. The other approach is to create a toMap() method in the class that needs to be converted to a Map that simply adds each field to the returned map using the name of the field.


1 Answers

The reason what you've shown is unsafe is that with this assignment:

Class<T> type = typeMap.get(key); 

T does not need to have anything to do with the Class retrieved from the map. T is always inferred from the surrounding context of the call to get. For example I can do this sequence of calls:

// T is inferred from the arguments as String (which is fine) example.put("k", "v"); // T is inferred from the return value target type as Integer Integer i = example.get("k"); 

Inside the get method, String.class is correctly retrieved from the type map, but an unchecked conversion is made to Class<Integer>. The call to type.cast(...) doesn't throw, because the value retrieved from the data map is a String. An implicit checked cast then actually happens to the return value, casting it to Integer and a ClassCastException is thrown.

This strange interaction is due to type erasure.


So, when we are storing multiple types in a single data structure, there are a number of ways to approach it, depending on what our needs are.

1. We can forego compilation checks if there is no way to perform them.

Storing the Class is pointless for the most part here because, as I showed above, it doesn't perform a useful validation. So we could redesign the map along the following lines:

class Example {     private final Map<String, Object> m = new HashMap<>();      void put(String k, Object v) {         m.put(k, v);     }      Object getExplicit(String k) {         return m.get(k);     }      @SuppressWarnings("unchecked")     <T> T getImplicit(String k) {         return (T) m.get(k);     } } 

getExplicit and getImplicit do a similar thing but:

String a = (String) example.getExplicit("k"); // the generic version allows an implicit cast to be made // (this is essentially what you're already doing) String b = example.getImplicit("k"); 

In both cases we're just relying on our own awareness as a programmer to not make mistakes.

Suppressing warnings isn't necessarily bad, it's just important to understand what they actually mean and what the implications are.

2. Pass a Class to get so the return value must be valid.

This is the way I've seen it done typically.

class Example {     private final Map<String, Object> m = new HashMap<>();      void put(String k, Object v) {         m.put(k, v);     }      <T> T get(String k, Class<T> c) {         Object v = m.get(k);         return c.isInstance(v) ? c.cast(v) : null;     } }  example.put("k", "v"); // returns "v" String s = example.get("k", String.class); // returns null Double d = example.get("k", Double.class); 

But, of course, it means we need to pass two parameters to get.

3. Parameterize the keys.

This is a novel but more advanced and it may or may not be more convenient.

class Example {     private final Map<Key<?>, Object> m = new HashMap<>();      <V> Key<V> put(String s, V v) {         Key<V> k = new Key<>(s, v);         put(k, v);         return k;     }      <V> void put(Key<V> k, V v) {         m.put(k, v);     }      <V> V get(Key<V> k) {         Object v = m.get(k);         return k.c.isInstance(v) ? k.c.cast(v) : null;     }      static final class Key<V> {         private final String k;         private final Class<? extends V> c;          @SuppressWarnings("unchecked")         Key(String k, V v) {             // this cast will always be safe unless             // the outside world is doing something fishy             // like using raw types             this(k, (Class<? extends V>) v.getClass());         }          Key(String k, Class<? extends V> c) {             this.k = k;             this.c = c;         }          @Override         public int hashCode() {             return k.hashCode();         }          @Override         public boolean equals(Object o) {             return (o instanceof Key<?>) && ((Key<?>) o).k.equals(k);         }     } } 

So e.g.:

Key<Float> k = example.put("k", 1.0f); // returns 1.0f Float f = example.get(k); // returns null Double d = example.get(new Key<>("k", Double.class)); 

This might make sense if the entries are known or predictable so we can have something like:

final class Keys {     private Keys() {}     static final Key<Foo> FOO = new Key<>("foo", Foo.class);     static final Key<Bar> BAR = new Key<>("bar", Bar.class); } 

Then we don't have to construct a key object any time a retrieval is done. This works very well especially for adding some strong typing to stringly-typed scenarios.

Foo foo = example.get(Keys.FOO); 

4. Don't have a map any kind of object can be put in, use polymorphism of some kind.

When possible and not too cumbersome, it's a good option. If there is a common behavior that the different types are used for, make it an interface or superclass so we don't have to use casting.

A simple example might be like this:

// bunch of stuff Map<String, Object> map = ...;  // store some data map.put("abc", 123L); map.put("def", 456D);  // wait awhile awhile();  // some time later, consume the data // being particular about types consumeLong((Long) map.remove("abc")); consumeDouble((Double) map.remove("def")); 

And we could instead substitute something like this:

Map<String, Runnable> map = ...;  // store operations as well as data // while we know what the types are map.put("abc", () -> consumeLong(123L)); map.put("def", () -> consumeDouble(456D));  awhile();  // consume, but no longer particular about types map.remove("abc").run(); map.remove("def").run(); 
like image 163
Radiodef Avatar answered Sep 22 '22 09:09

Radiodef