Map with multiple value types with advantages of generics

casting, compiler-errors, dictionary, generics, java

Solution

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.

Problem

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: - compile time warnings on putting wrong things into the collection - no need to cast when getting things out of collections So what I want is a map: - which supports multiple value objects, - checks values put into the map (preferably at compile-time) - knows what object values are when getting from the 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: - Why doesn't `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: - If you think this is a bad design - can you explain why? - I've used String and Integer as example value types - in reality I have a multitude of different Key / value type pairs that I would like to be able to use. I want to use these in a single map - that's the objective.

Original source