I want to create a map that will provide the benefits of generics, whilst supporting multiple different types of values. I consider the following to be the two key advantages of generic collections:
So what I want is a map:
The base case, using generics, is:
Map<MyKey, Object> map = new HashMap<MyKey, Object>();
// No type checking on put();
map.put(MyKey.A, "A");
map.put(MyKey.B, 10);
// Need to cast from get();
Object a = map.get(MyKey.A);
String aStr = (String) map.get(MyKey.A);
I've found a way to resolve the second issue, by creating an AbstractKey, which is generified by the class of values associated with this key:
public interface AbstractKey<K> {
}
public enum StringKey implements AbstractKey<String>{
A,B;
}
public enum IntegerKey implements AbstractKey<Integer>{
C,D;
}
I can then create a TypedMap, and override the put() and get() methods:
public class TypedMap extends HashMap<AbstractKey, Object> {
public <K> K put(AbstractKey<K> key, K value) {
return (K) super.put(key, value);
}
public <K> K get(AbstractKey<K> key){
return (K) super.get(key);
}
}
This allows the following:
TypedMap map = new TypedMap();
map.put(StringKey.A, "A");
String a = map.get(StringKey.A);
However, I don't get any compile errors if I put in the wrong value for the key. Instead, I get a runtime ClassCastException on get().
map.put(StringKey.A, 10); // why doesn't this cause a compile error?
String a = map.get(StringKey.A); // throws a ClassCastException
It would be ideal if this .put() could give a compile error.
As a current second best, I can get the runtime ClassCastException to be thrown in the put() method.
// adding this method to the AbstractKey interface:
public Class getValueClass();
// for example, in the StringKey enum implementation:
public Class getValueClass(){
return String.class;
}
// and override the put() method in TypedMap:
public <K> K put(AbstractKey<K> key, K value){
Object v = key.getValueClass().cast(value);
return (K) super.put(key, v);
}
Now, the ClassCastException is thrown when put into the map, as follows. This is preferable, as it allows easier/faster debugging to identify where an incorrect key/value combination has been put into the TypedMap.
map.put(StringKey.A, 10); // now throws a ClassCastException
So, I'd like to know:
map.put(StringKey.A, 10) cause a compile error?How could I adapt this design to get meaningful compile errors on put, where the value is not of the associated generic type of the key?
Is this is a suitable design to achieve what I want (see top)? (Any other thoughts/comments/warnings would also be appreciated...)
Are there alternative designs that I could use to achieve what I want?
EDIT - clarifications:
You are messing with generics and overloading in a bad way. You are extending HashMap<AbstractKey, Object> and so your class is inheriting the method Object put(AbstractKey k, Object v). In your class you are defining another put method with a different signature, which means you are just overloading the put method, instead of overriding it.
When you write map.put(StringKey.A, 10), the compiler tries to find a method that conforms to the argument types put(StringKey, Integer). Your method's signature doesn't apply, but the inherited put's does -- StringKey is compatible with AbstractKey and Integer is compatible with Object. So it compiles that code as a call to HashMap.put.
A way to fix this: rename put to some custom name, like typedPut.
BTW talking from experience your approach is very fun and engaging, but in real life it just isn't worth the trouble.
Item 29: Consider typesafe heterogeneous containers.—Joshua Bloch, Effective Java, Second Edition, Chapter 5: Generics.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With