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));
}
}
HashMap<Foo, Bar>?HashMap<Object, Object>with custom "put".TypeLiterals are doing.