Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Map with multiple value types with advantages of generics

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.
like image 233
amaidment Avatar asked May 03 '12 10:05

amaidment


2 Answers

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.

like image 56
Marko Topolnik Avatar answered Oct 21 '22 10:10

Marko Topolnik


Item 29: Consider typesafe heterogeneous containers.—Joshua Bloch, Effective Java, Second Edition, Chapter 5: Generics.

like image 41
trashgod Avatar answered Oct 21 '22 10:10

trashgod