Wouldn’t it be nice if you could do this:
TypedMap map = new TypedMap(); String expected = "Hallo"; map.set( KEY1, expected ); String value = map.get( KEY1 ); // Look Ma, no cast! assertEquals( expected, value ); List<String> list = new ArrayList<String> (); map.set( KEY2, list ); List<String> valueList = map.get( KEY2 ); // Even with generics assertEquals( list, valueList );
Note: The type checking is at compile time. No runtime cost!
As you can see, I get different types from the map without casting. How is that possible? Well, Generics can be your friends. The magic is in the key:
final static TypedMapKey<String> KEY1 = new TypedMapKey<String>( "key1" ); final static TypedMapKey<List<String>> KEY2 = new TypedMapKey<List<String>>( "key2" );
The keys contains two pieces of information: The actual key (a string) and the type which the value of the key has. The class for the key is completely braindead:
public class TypedMapKey<T> { private String name; public TypedMapKey(String name) { this.name = name; } public String name() { return name; } }
The trick is to use the auto-resolution of the type at compile time in TypedMap. As you can see below, I need to suppress warnings about a typecast but the nice thing is: I only have to do it in this central place and the annotation is always correct (well, until you start to use set(String)).
As you can also see, I use delegation. I could have extended map but I wanted to show a pattern which you can use in other places … like HttpRequest or HttpSession.
import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Set; public class TypedMap implements Map<String, Object> { private Map<String, Object> delegate; public TypedMap( Map<String, Object> delegate ) { this.delegate = delegate; } public TypedMap() { this.delegate = new HashMap<String, Object>(); } @SuppressWarnings( "unchecked" ) public <T> T get( TypedMapKey<T> key ) { return (T) delegate.get( key.name() ); } @SuppressWarnings( "unchecked" ) public <T> T remove( TypedMapKey<T> key ) { return (T) delegate.remove( key.name() ); } public <T> void put( TypedMapKey<T> key, T value ) { delegate.put( key.name(), value ); } // --- Only calls to delegates below public void clear() { delegate.clear(); } public boolean containsKey( Object key ) { return delegate.containsKey( key ); } public boolean containsValue( Object value ) { return delegate.containsValue( value ); } public Set<java.util.Map.Entry<String, Object>> entrySet() { return delegate.entrySet(); } public boolean equals( Object o ) { return delegate.equals( o ); } public Object get( Object key ) { return delegate.get( key ); } public int hashCode() { return delegate.hashCode(); } public boolean isEmpty() { return delegate.isEmpty(); } public Set<String> keySet() { return delegate.keySet(); } public Object put( String key, Object value ) { return delegate.put( key, value ); } public void putAll( Map<? extends String, ? extends Object> m ) { delegate.putAll( m ); } public Object remove( Object key ) { return delegate.remove( key ); } public int size() { return delegate.size(); } public Collection<Object> values() { return delegate.values(); } }
Update 28. June 2010: As noticed by a couple of people on StackOverflow, this isn’t type safe if you define two keys with the same name.
Note that the code above is to wrap an existing map with a more type-safe API. If you want to make this even more type safe, create the map like so:
import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Set; public class TypedMap implements Map<TypedMapKey<?>, Object> { private Map<TypedMapKey<?>, Object> delegate; public TypedMap( Map<TypedMapKey<?>, Object> delegate ) { this.delegate = delegate; } public TypedMap() { this.delegate = new HashMap<TypedMapKey<?>, Object>(); } @SuppressWarnings( "unchecked" ) public <T> T get( TypedMapKey<T> key ) { return (T) delegate.get( key.name() ); } ... }