In my previous post I explored how we can have the type of a map value be correlated with the type of its key.
We reached a design where a class contains a Level Three implementation, with Level One client code.This is all well and good, but in a real project this design will probably expand in two directions simultaneously:
- Instances: we may need to copy-paste-modify the
ConverterLibrary
(that mapsPair<Class<F>,Class<T>>
toFunction<F,T>
) class to create libraries of formatters (Class<T>
toParse<T>
), parsers, CRUD services, etc. The keys need not be class objects: we can have prototype maps (T
toT
), maps associatingList<T>
to keys of typeSupplier<T>
(e.g. recording values previously returned by the supplier or inversely that the given supplier needs to return), etc. - Features: Clients may need methods like
containsKey
,remove
,keySet
, etc. This entails dangerously writing more Level Three code, potentially copy-pasting from one instance to another (previous point), potentially forgetting to update all parts of the code.
Can we capture the pattern of ConverterLibrary
in a single class, with a single implementation of put
, get
, putAll
but also remove
, containsKey
, size
etc, all that without specifying the Pair<Class...>
and Function
stuff, so that it may be safely instantiated for all the above examples?
The trick is to recognise that there are two ways in Java to enforce correlation between two type parameters.
The first way, that was used by the methods in the previous section, is to use type parameters in a method:
public <F,T> void put( Class<F> from, Class<T> to, Function<F,T> converter );
In this signature, the two type parameters permit enforcing a correlation between the key and value type parameters.
The other way, that we are going to use now, is in the type parameters of a type (class or interface), which may enforce correlations between types of its attributes (yet another way, is correlations between parameter of superclass type parameters). This is a class written to look as much as possible like the above method:
public class Put<F,T> { Class<F> from; Class<T> to; Function<F,T> converter; }
In addition to being a suitable parameter to a generic put
method, an instance of this class can be used to represent an entry of a converter map, the map itself being a collection of such entries. Renaming the above and rewriting to use a generic Entry
superclass we get this:
public class Entry<K,V> { public final K key; public final V value; public Entry(K key, V value) { this.key=key; this.value=value; } } public class ConverterEntry<F,T> extends Entry< Pair<Class<F>,Class<T>>, Function<F,T> > {}
As this class not only records the key and value types, but also all correlations between them, we know it is sufficient to use it as type parameter:
public class CorrelatedMap<E> {...} public class ConverterLibrary extends CorrelatedMap<ConverterEntry<?,?>> {...}
If we don’t need a return value to the put
method, it may be declared like this:
public class CorrelatedMap<E> { public void put(E entry) {...} //... }
But how can we get a type-safe get
method that would cause a compilation error if misused?
We could do this:
public E get(E entry) { // return an entry having the same key as the given one. }
That works fine, but when passing a ConverterEntry<Fruit,Juice>
we still get a ConverterEntry<?,?>
in return because that’s what E
is set to in ConverterLibrary
.
OR, we could do the same but with the following signature:
public <K,V> Entry<K,V> get(Entry<K,V> entry);
But then when passing a ConverterEntry<Fruit,Juice>
we get an Entry<Pair<Class<Fruit>,Class<Juice>,Function<Fruit,Juice>>
. Somewhat better (as, assuming a getValue()
method in Entry
, we can get the Function
without casting back and forth). However there’s no verification we’re actually passing a ConverterEntry
.
Can we do intersection types?
public <K, V, T extends E & Entry<K,V>> T get(T entry);
No, the compiler complains that T
attempts to derive both Entry<?,?>
(because that’s what E
resolves to) and Entry<K,V>
.
All these problems stem from Entry
appearing in the CorrelatedMap
having type parameters, that we’re forced to set to “?
“. The trick is therefore to create a superinterface without type parameters:
Entry types:
public interface HasKey { public Object getKey(); } public class Entry<K,V> { public final K key; public final V value; public Entry(K key, V value) { this.key=key; this.value=value; } public K getKey() { return key; } } public interface ConverterEntry extends HasKey {} public class TypedConverterEntry<F,T> extends Entry< Pair<Class<F>,Class<T>>, Function<F,T> > { public TypedConverterEntry(Class<F> from, Class<T> to) { super(Pair.of(from, to), null); } public TypedConverterEntry(Class<F> from, Class<T> to, Function<F,T> converter) { super(Pair.of(from, to), converter); } }
Map types:
public class CorrelatedMap<E extends HasKey> { Map<Object, E> entries = new HashMap<>(); public <T extends E> T get(T query) { return (T)entries.get(query.getKey()); } public <T extends E> T put(T entry) { return (T)entries.put(entry.getKey(), entry); } public boolean putAll(CorrelatedMap<E> that) { return entries.putAll(that.entries); } //... } public class ConverterLibrary extends CorrelatedMap<ConverterEntry> { public <F,T> Function<F,T> get(Class<F> from, Class<T> to) { return getValue(super.get(new TypedConverterEntry<>(from,to))); } public <F,T> Function<F,T> put(Class<F> from, Class<T> to, Function<F,T> converter) { return getValue(super.put(new TypedConverterEntry<>(from, to, converter))); } private static <F,T> Function<F,T> getValue(TypedConverterEntry<F,T> e) { return (e==null)? null : e.value; } }
Methods in ConverterLibrary
are there for convenience, to avoid its clients having to construct and deconstruct TypedConverterEntries, but unlike the previous implementation it is Level One: the (Class<F>, Class<T>)
parameter types force the TypedConverterEntry
to type TypedConverterEntry<F, T>
, which is assigned to the type parameter E
of the put
method, which returns the same TypedConverterEntry<F, T>
type, whose value
attribute is correctly inferred to have type Function%lt;F, T>
.
I am not very happy with having to define two types ConverterEntry
and TypedConverterEntry
, as well as having to invoke getValue
from the subclass ConverterLibrary
instead of the superclass. Is it possible to do better while still retaining Level One safety? I do not yet have the answer to that question.