Use Java HashMap multiple Type

2.3k views Asked by At

I'm using a Hashmap as my in-memory cache. So basically, this is what I have:

private static final Map<String, Object> lookup = new HashMap<String, Object>();

    public static Object get(CacheHelper key) {
        return lookup.get(key.getId());
    }

    public static void store(CacheHelper key, Object value) {
        lookup.put(key.getId(), value);
    }

That's fine. But for every Object I "get" from the Map, I have to cast, which is very ugly. I want to put ArrayList and many other different things into it.

Does anybody know an other solution to be typesafe ?

(For sure, I can create for every type a getter and setter, but is that the only solution ?

So, is there a better way to create an in-memory cache or does somebody have an idea how to wrap the hashmap to be more safe ?

5

There are 5 answers

0
Louis Wasserman On BEST ANSWER

One solution to this problem is to make your CacheHelper type generic, with CacheHelper<T>. Then create a wrapper for your map:

class MyCache {
  private final Map<CacheHelper<?>, Object> backingMap = new HashMap<>();
  public <T> void put(CacheHelper<T> key, T value) {
    backingMap.put(key, value);
  }
  @SuppressWarnings("unchecked")
  // as long as all entries are put in via put, the cast is safe
  public <T> T get(CacheHelper<T> key) {
    return (T) backingMap.get(key);
  }
}

The Java compiler actually uses this approach internally; see e.g. here. You don't have to pass around explicit Class objects, but you do have to know what type is actually associated with each key, which is as it should be in well-behaved applications.

0
Thomas On

You could store the type of the elements ass well, pass the type you expect to the get() method and check there. AFAIK there's no built-in way to make storing different types typesafe without some common superclass or interface.

Basically you'd do this:

private static final Map<String, Object> lookup = new HashMap<>();
private static final Map<String, Class<?>> types= new HashMap<>();   

public static <T> T get(CacheHelper key, Class<T> expectedType ) {
  Class<?> type = types.get(key.getId());
  if( type == null || expectedType == null || expectedType.isAssignableFrom( type ) ) {
    throw new IllegalArgumentException("wrong type");
  }   
  return (T)lookup.get(key.getId());
}

//if null values should be allowed, you'd need to change the signature to use generics 
//and pass the expected type as well, e.g. <T> void store(CacheHelper key, T value, Class<T> type) 
public static void store(CacheHelper key, Object value ) {
    lookup.put(key.getId(), value);
    types.put( key.getId(), value.getClass() );
}

Note that this still wouldn't catch any compile time errors since you're basically disabling generics inside your cache. Without a common superclass/interface or a generic type on cache itself there's no compile time way to catch those errors - at least no easy one.

If you just don't want to do the casts yourself you can hide them inside get() and live with the possible class cast exception. In that case you can let the compiler infer the type of T from the call (by assignment, explicitly set or type of the parameter).

However, passing the expected class easily provides additional information that you can use to create better error messages etc. which you'd not get from a ClassCastException.

0
afzalex On

You can use generics feature if java version is 7 (or upper)

public static <T> T get(CacheHelper key){
    return (T)(lookup.get(key.getId());
}

And you can then call it as follows :

Myclass.<String>get(key);

In the above example I supposed the Myclass be a class containing the get() method

0
Dici On

You can pass a Class object as a parameter and use the Class.cast method :

public static <T> T get(CacheHelper key, Class<T> clazz){
    return clazz.cast(lookup.get(key.getId());
}
0
Joop Eggen On

The get needs a Class<T>, as because of type erasure a cast (T) is a senseless no-op.

Either incorporate the Class all the way; IDs per class:

private final Map<Class<?>, Map<String, Object>> lookup = new HashMap<>();

public <T> T get(Class<T> klass, String id) {
    Map<String, Object> mapById = lookup.get(klass);
    if (mapById == null) {
        return null;
    }
    Object value = mapById.get(id);
    return klass.cast(value);
}

public <T> void store(Class<T> klass, String id, T value) {
    Map<String, Object> mapById = lookup.get(klass);
    if (mapById == null) {
        mapById = new HashMap<>();
        lookup.put(klass, mapById);
    }
    mapById.put(id, value);
}

The store not using value.getClass() to allow a get by interface or base class:

store(Number.class, "x", 3.14);
store(Number.class, "x", 3);
store(Number.class, "x", new BigDecimal("3.14"));

Or do only

public static <T> T get(Class<T> klass, String id) {
    Object value = lookup.get(id);
    return klass.cast(value);
}

(I simplified the original code a bit, for the readers.)