Type-safe object map

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() );
    }
...
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 333 other followers