Enforce compile type safety with TypeSafeMap

Viewed 58

Problem statement: We are building a library which has a TypeSafeMap as response. TypeSafeMap is a map which can hold any type of objects. Now, the client is going to access the typesafemap. We are trying to enforce some level of compile type safety. Below is the code and more explanation.

Response structure:

public Class Response {
    private TypeSafeMap t;
    
    public TypeSafeMap getMap() { return t; }
}

//Type safety Map

public class TypeSafeMap 
{
    private final static Map<String, Object> map = new HashMap<>();
    
    public static <T> T put(String key, T value) {
        if (null != key) {
            return (T) map.put(key, value);
        }
        return (T) map;
    }

    @SuppressWarnings("unchecked")
    public static <T> T get(PartyEnums partyEnum)
    {       
        return (T) map.get(partyEnum.PARTY.name());
    }

}

//Enum that we expose to client to get the property and the corresponding field type

public enum PartyEnums
{   
    PARTY("party", new ArrayList<Party>().getClass()); 
    
    private final String name;
    private final Class<?> clzz; //this is the type client should access as field type

    PartyEnums(String name,Class<?> clzz) 
    {
        this.name = name;
        this.clzz=clzz;
    }
    
    public Class<?> getClzz()
    {
        return clzz;
    }
    
    @SuppressWarnings("unchecked")
    public <T> T getInstance()
    {
        T ins = null;
        try {
            ins = (T) getClzz().newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return ins;
    }
}

//Client call

    public class ClientCall {
        Object obj = TypeSafeMap.get(PartyEnums.PARTY); //No error. 
        String str = TypeSafeMap.get(PartyEnums.PARTY); //No error. 
But we want enforce some level of compile type safety as the field type "str" and TypeSafeMap.get() type do not match. 
How can we enforce compile type safety?
     
       List<Party> party = TypeSafeMap.get(PartyEnums.PARTY);// OK.
    }
1 Answers

TL;DR

This cannot be done using an enum in its current form. There was a JEP 301: Enhanced Enums that would address the very same problem, but it's been withdrawn.


Your only choice to enforce type-safety (currently) is to simulate an enum by using a final class with predefined typed constants. I would imagine it to be something like this:

public final class Key<V> {
    public static final Key<List<Party>> PARTY_LIST = new Key<>("party list");
    public static final Key<Map<Integer, String>> PARTY_MAP = new Key<>("party map");
    public static final Key<String> SOME_STRING = new Key<>("party string");

    private final String name;
    
    private Key(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
}

And now we are ready to make your TypeSafeMap really type-safe by modifying both <T> T put(String key, T value) (which would not obviously be safe even if the "enum approach" was theoretically possible) and <T> T get(PartyEnums partyEnum) this way:

public static class TypeSafeMap {
    private final static Map<String, Object> MAP = new HashMap<>();

    @SuppressWarnings("unchecked")
    public static <T> T put(Key<T> key, T value) {
        if (null != key) {
            return (T) MAP.put(key.getName(), value);
        }
        
        return value;
    }

    @SuppressWarnings("unchecked")
    public static <T> T get(Key<T> key) {
        return (T) MAP.get(key.getName());
    }
}

I still see at least 2 possible issues that:

  1. Multiple constants may share the same name hence effectively overwriting each other once used (can be tackled for instance using an enum instead of String)
  2. put(Key<T> key, T value) passes through the input value back even for the null keys which is (in my opinion) fail-safe but misleading for the caller (who might think the "store" operation was somehow succeeded.

However, given the weak points above this implementation still fits your original intention, so that:

These cases would pass:

    map.put(Key.JUST_STRING, "just string");
    map.put(Key.PARTY_LIST, Arrays.asList(new Party()));
           ...
    Object resultAsObject = map.get(Key.PARTY_LIST);
    List<Party> resultAsList = map.get(Key.PARTY_LIST);
    String resultString = map.get(Key.JUST_STRING);

But these fail with compile-time error:

    map.put(Key.PARTY_LIST, Arrays.asList(new String()));
           ...
    String stringFromList = map.get(Key.PARTY_LIST);

Note that Object resultAsObject = map.get(<ANYTHING>) will always succeed as any returned value (including nulls) can we represented as an Object variable

Related