1

I am looking at creating a Hashmap class which allows me to store keys and values. However, the value can only be stored if it matches a specific type, and the type is dependent on the runtime value of the key. For example, if the key is EMAIL(String.class), then the stored value should be of type String.

I have following custom ENUM:

public enum TestEnum {
    TDD,
    BDD,
    SHIFT_RIGHT,
    SHIFT_LEFT;
}

I have created following class :

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

public class test {

    private static final Map<ValidKeys, Object> sessionData = new HashMap<>();

    public enum ValidKeys {
        EMAIL(String.class),
        PASSWORD(String.class),
        FIRST_NAME(String.class),
        LAST_NAME(String.class),
        CONDITION(TestEnum.class);

        private Class type;
        private boolean isList;
        private Pattern pattern;

        ValidKeys(Class<?> type, boolean isList) {
            this.type = type;
            this.isList = isList;
        }

        ValidKeys(Class<?> type) {
            this.type = type;
        }
    }

    public <T> void setData(ValidKeys key, T value) {
        sessionData.put(key,value);
    }


    public Object getData(ValidKeys key) {
        return key.type.cast(sessionData.get(key));
    }


    public static void main(String[] args) {
        test t = new test();
        t.setData(ValidKeys.CONDITION,TestEnum.TDD);
        System.out.println(t.getData(ValidKeys.CONDITION));
    }
}

I would like to use methods such as setData and getData and store values into sessionData. Also, I want to ensure if the value is a list of objects then thats stored properly as well.

I am also struggling to avoid toString basically I need a generic getData which can work without type casting.

17
  • 1
    And your question is? Commented Dec 19, 2018 at 23:34
  • 1
    So, what prevents you from declaring your hash map as HashMap<Foo, Bar>? Commented Dec 19, 2018 at 23:41
  • 1
    Ever occurred to you to read the Javadoc, or to learn about generics? Commented Dec 19, 2018 at 23:43
  • 1
    @AndreyTyukin, it looks like he wants to have a map, that can hold mixed key/value pair of any type, but the type check will be performed later while putting something to this map. It's like HashMap<Object, Object> with custom "put". Commented Dec 19, 2018 at 23:45
  • 1
    @JavaMan Ah, ok, I see now. You are trying to save runtime type-tags in the keys of the hash map, and those type-tags have to be able to differentiate between various types and lists of other types. Similar to what Guice's TypeLiterals are doing. Commented Dec 20, 2018 at 1:02

1 Answer 1

1

There is a particular pattern I've seen used for this sort of thing, which is a variant of Bloch's Typesafe Heterogenous Container pattern. I don't know if it has a name on its own or not, but for lack of a better name I'll call it Typesafe Enumerated Lookup Keys.

Basically, a problem that I've seen arise in various contexts is where you want a dynamic set of key/value pairs, where a particular subset of keys are "well-known" with predefined semantics. Additionally, each key is associated with a particular type.

The "obvious" solution is to use an enum. For example, you could do:

public enum LookupKey { FOO, BAR }

public final class Repository {
    private final Map<LookupKey, Object> data = new HashMap<>();

    public void put(LookupKey key, Object value) {
        data.put(key, value);
    }

    public Object get(LookupKey key) {
        return data.get(key);
    }
}

This works just fine, but the obvious drawback is that now you need to cast everywhere. For example, suppose you know that LookupKey.FOO always has a String value, and LookupKey.BAR always has an Integer value. How do you enforce that? With this implementation, you can't.

Also: with this implementation, the set of keys is fixed by the enum. You can't add new ones at runtime. For some applications that's an advantage, but in other cases you really do want to allow new keys in certain cases.

The solution to both these problems is basically the same one: make LookupKey a first-class entity, not just an enum. For example:

/**
 * A key that knows its own name and type.
 */
public final class LookupKey<T> {
    // These are the "enumerated" keys:
    public static final LookupKey<String> FOO = new LookupKey<>("FOO", String.class);
    public static final LookupKey<Integer> BAR = new LookupKey<>("BAR", Integer.class);

    private final String name;
    private final Class<T> type;

    public LookupKey(String name, Class<T> type) {
        this.name = name;
        this.type = type;
    }

    /**
     * Returns the name of this key.
     */
    public String name() {
        return name;
    }

    @Override
    public String toString() {
        return name;
    }

    /**
     * Cast an arbitrary object to the type of this key.
     * 
     * @param object an arbitrary object, retrieved from a Map for example.
     * @throws ClassCastException if the argument is the wrong type.
     */
    public T cast(Object object) {
        return type.cast(object);
    }

    // not shown: equals() and hashCode() implementations
}

This gets us most of the way there already. You can refer to LookupKey.FOO and LookupKey.BAR and they behave like you would expect, but they also know the corresponding looked-up type. And you can also define your own keys by creating new instances of LookupKey.

If we want to implement some nice enum-like abilities like the static values() method, we just need to add a registry. As a bonus, we don't even need equals() and hashCode() if we add a registry, since we can just compare lookup keys by identity now.

Here's what the class ends up looking like:

/**
 * A key that knows its own name and type.
 */
public final class LookupKey<T> {
    // This is the registry of all known keys.
    // (It needs to be declared first because the create() function needs it.)
    private static final Map<String, LookupKey<?>> knownKeys = new HashMap<>();

    // These are the "enumerated" keys:
    public static final LookupKey<String> FOO = create("FOO", String.class);
    public static final LookupKey<Integer> BAR = create("BAR", Integer.class);


    /**
     * Create and register a new key. If a key with the same name and type
     * already exists, it is returned instead (Flywheel Pattern).
     *
     * @param name A name to uniquely identify this key.
     * @param type The type of data associated with this key.
     * @throws IllegalStateException if a key with the same name but a different
     *     type was already registered.
     */
    public static <T> LookupKey<T> create(String name, Class<T> type) {
        synchronized (knownKeys) {
            LookupKey<?> existing = knownKeys.get(name);
            if (existing != null) {
                if (existing.type != type) {
                    throw new IllegalStateException(
                            "Incompatible definition of " + name);
                }
                @SuppressWarnings("unchecked")  // our invariant ensures this is safe
                LookupKey<T> uncheckedCast = (LookupKey<T>) existing;
                return uncheckedCast;
            }
            LookupKey<T> key = new LookupKey<>(name, type);
            knownKeys.put(name, key);
            return key;
        }
    }

    /**
     * Returns a list of all the currently known lookup keys.
     */
    public static List<LookupKey<?>> values() {
        synchronized (knownKeys) {
            return Collections.unmodifiableList(
                    new ArrayList<>(knownKeys.values()));
        }
    }

    private final String name;
    private final Class<T> type;

    // Private constructor. Only the create method should call this.
    private LookupKey(String name, Class<T> type) {
        this.name = name;
        this.type = type;
    }

    /**
     * Returns the name of this key.
     */
    public String name() {
        return name;
    }

    @Override
    public String toString() {
        return name;
    }

    /**
     * Cast an arbitrary object to the type of this key.
     * 
     * @param object an arbitrary object, retrieved from a Map for example.
     * @throws ClassCastException if the argument is the wrong type.
     */
    public T cast(Object object) {
        return type.cast(object);
    }
}

Now LookupKey.values() works more or less like an enum would. You can also add your own keys, and values() will return them afterward:

LookupKey<Double> myKey = LookupKey.create("CUSTOM_DATA", Double.class);

Once you have this LookupKey class, you can now implement a typesafe repository that uses these keys for lookup:

/**
 * A repository of data that can be looked up using a {@link LookupKey}.
 */
public final class Repository {
    private final Map<LookupKey<?>, Object> data = new HashMap<>();

    /**
     * Set a value in the repository.
     *
     * @param <T> The type of data that is being stored.
     * @param key The key that identifies the value.
     * @param value The corresponding value.
     */
    public <T> void put(LookupKey<T> key, T value) {
        data.put(key, value);
    }

    /**
     * Gets a value from this repository.
     *
     * @param <T> The type of the value identified by the key.
     * @param key The key that identifies the desired value.
     */
    public <T> T get(LookupKey<T> key) {
        return key.cast(data.get(key));
    }
}
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks @Daniel Pryden, looks really cool and seems to have nailed the issue around Object / explicit casting. Awesome, Cheers !!
A quick one, I have no idea why I see a NPE for KnownKeys in create method. I have resolved it by initialising it again in the create method, but am not sure if that's correct. Pointers?
My bad, I had it laid out differently before and I reorganized the code when I posted it here. The knownKeys map needs to be declared before the "enumerated" values, since static fields are initialized in lexical order. I'll update the answer to show this. (As you point out, the other way to solve this is to initialize it lazily in the create method, but then it can't be final.)
Thanks a lot Daniel Pryden for responding to my comment. I should have picked it up yesterday whilst debugging but just didn't stuck me. Awesome, cheers !!!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.