A genuinely type safe Map for Java

Steve Neal Java Programming Leave a Comment

The problem:

As much as I love generics in Java, it is still necessary to employ evil casts when retrieving different data types from a Map; see lines 13, 14 and 15:

//declare the keys
final String ID = "ID";
final String NAME = "NAME"
final String AGE = "AGE";

//put data in the map
Map<String, Object> studentData = new HashMap<>();
studentData.put(ID, 123L);
studentData.put(NAME, "FRED");
studentData.put(AGE, 21);

//lookup data in the map using copious type casting
String id = (String) studentData.get(ID);
String name = (String) studentData.get(NAME);
int age = (Integer) studentData.get(AGE);

Did you spot the bug in this example? The compiler would have missed it.
Ideally, I’d much rather work with code like this:

//lookup data in the map
long id = studentData.get(ID);
String name = studentData.get(NAME);
int age = studentData.get(AGE);

In addition to the casts being eliminated, I’d like the compiler to catch any potential problems with incorrect data types being added and retrieved too.

The motivation:

I’ve spent the past few weeks building a data grid system using Apache Ignite, and in order to maximise portability of the data, we have made the decision that it will all be held in Maps. No domain classes will be used anywhere. This makes a truly type-safe map a very attractive proposition.

The solution I’ve devised is simple and effective and will allow us to achieve map based storage for all data, while retaining the type safety found when using POJO domain classes. Read on for more details.

The solution:

Here is an (abridged) implementation of a Map that will solve the problem of casting when retrieving entries of different types:

public class TypeSafeMap {

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

    public <T> T put(Key<T> key, T value) {
        return (T) map.put(key.getName(), value);
    }

    public <T> T get(Key<T> key) {
        return (T) map.get(key.getName());
    }

    /**
     * Keys for the map, each of which is parameterised by a type.
     *
     * @param <T> the Class of object that this key will store/retrieve.
     */
    public static class Key<T> {
        private final String name;

        public Key(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }
}

Note that both the put and the get method use Key objects for their keys, and that Key is defined as an inner class. Interestingly, the Key class can be parameterised with a type.
Looking back at the put and get methods, you will see that it is the type of the Key which restricts the types of values that are stored and retrieved in the underlying Map.

All that remains is to replace the final String definitions used above with a constants interface listing the keys you need:

public interface StudentFields {
    static final TypeSafeMap.Key<Long> ID = new TypeSafeMap.Key<>("ID");
    static final TypeSafeMap.Key<String> NAME = new TypeSafeMap.Key<>("NAME");
    static final TypeSafeMap.Key<Integer> AGE = new TypeSafeMap.Key<>("AGE");
}

…and you can write type-safe code when working with the Map:

import static typesafemap.StudentFields.*;
 
TypeSafeMap map = new TypeSafeMap();
map.put(ID, 123L);
map.put(NAME, "FRED");
map.put(AGE, 21);

long id = map.get(ID);
String name = map.get(NAME);
int age = map.get(AGE);

You might even implement a version that uses the excellent Java 8 Optional class (just like in Guava and Scala) to ensure that values are checked for nullness when retrieved:

import static typesafemap.StudentFields.*;
 
TypeSafeMap map = new TypeSafeMap();
map.put(ID, 123L);
map.put(NAME, "FRED");
map.put(AGE, 21);

Optional<Long> id = map.get(ID);
Optional<String> name = map.get(NAME);
Optional<Integer> age = map.get(AGE);
A working copy of this code can be downloaded from here.

Leave a Reply

Your email address will not be published. Required fields are marked *