We’ve already discussed a maps whose value types depended on key values. Today’s topic is slightly different, and is about value types depending and key types.
Now that types may be parametrised with generics, it is a common requirements that types may be correlated, that is, share an unspecified parameter. For instance you could map pairs of classes to conversion functions:
For an input pair
Pair<Class<Fruit>,Class<Juice>> mixerSpecification = Pair.of(Fruit.class, Juice.class);
we want a converter
Function<Fruit,Juice> mixer;
that we can use as follows:
Fruit orange = ...; Juice orangeJuice = mixer.apply(orange);
In other words the input type Pair<Class<F>,Class<T>>
is correlated with the output type Function<F,T>
as the input type parameters F
and T
are arbitrary, but must be repeated in the output type parameters.
In addition to adding and getting converters we’ll also see how we can merge converter libraries, that is how a putAll
method can be implemented in the various implementations.
Level Seven (or Three)
The most accurate Java type we can use for this requirement is:
Map<Pair<Class<?>, Class<?>>, Function<?, ?>> converters = new HashMap<>();
Adding a new converter is done as follows:
Function<Fruit,Juice> mixer = ...; converters.put(Pair.<Class<?>,Class<?>>of(Fruit.class, Juice.class), mixer);
Note that we had to force the Pair
constructor function to build a Pair<Class<?>,Class<?>>
, because the inferred type would otherwise be Pair<Class<Fruit>,Class<Juice>>
, which can’t be cast (as required by the required by the Map.put
method) to the Map
‘s key type parameter that we set to Pair<Class<?>,Class<?>>
.
There is no compile-time or run-time verification of the correlation between the key and the value types, simply because we could not specify it in the Map
type specification.
Retrieving the converter given a pair of classes is done as follows:
@SuppressWarnings("unchecked") Function<Fruit,Juice> mixer = (Function<Fruit,Juice>) converters.get(Pair.of(Fruit.class,Juice.class));
Note that we do not need to construct a Pair<Class<?>,Class<?>>
this time, as Map.get
takes an Object
parameter. We however must cast the result from Function<?,?>
(the Map
‘s declared value type), which generates an “unchecked conversion” warning that we have to suppress with an annotation.
Although errors in types will be caught neither at compilation nor at run-time, this design could arguably be qualified to be Level Three. See the following code containing mistakes:
Function<Fruit,Colour> getColour = ...; converters.put(Pair.<Class<?>,Class<?>>of(Fruit.class, Juice.class), getColour); //... @SuppressWarnings("unchecked") Function<Fruit,Colour> getColour = (Function<Fruit,Colour>) converters.get(Pair.of(Fruit.class,Juice.class));
Are the two mistakes (mismatch between Colour
and Juice
) “visually obvious”? In the first case there may be other things creeping up between the declaration of getColour
(which could e.g. be a method parameter) and its use in the put
method. The second one suppresses warnings, which generally means any Level Two design in use in the scope of the annotation is downgraded to Level Seven. As there is no code beside the Pair
construction and get
invocation, it should however be safe as long as no enterprising programmer moves all such annotations up to method or class level to save lines of code.
The putAll
method specified in the requirements of this post is very easy to write:
Map<Pair<Class<?>,Class<?>>, Function<?,?>> externalLibrary=...; converters.putAll(externalLibrary);
However it provides no guarantees as to what internal constraints exist in externalLibrary
. If they do not match those of converters
, the internal consistency of converters
will be destroyed, potentially resulting in hard-to-track errors.
Contain Unsafe Code; Keep Outside Code Level One
One way of addressing issues stemming from “unsafe” code is to gather it in a single class with an interface enforcing desired type constraints, to avoid bugs appearing in a copy-paste-modify sequence. In this case it means encapsulating the Map
in a class like this:
public class ConverterLibrary { private Map<Pair<Class<?>, Class<?>>, Function<?, ?>> converters = new HashMap<>(); @SuppressWarnings("unchecked") public <F,T> Function<F,T> put(Class<F> from, Class<T> to, Function<? super F, ? extends T> converter) { return (Function<F,T>) converters.put(Pair.<Class<?>, Class<?>>of(from, to), converter); } @SuppressWarnings("unchecked") public <F,T> Function<F,T> get(Class<F> from, Class<T> to) { return (Function<F,T>) converters.get(Pair.of(from, to)); } public boolean putAll(ConverterLibrary that) { return this.converters.putAll(that.converters); } }
As a bonus we made the put
method more tolerant on values. This permits passing more general functions:
Function<Object,String> toString = ...; converterLibrary.put(Fruit.class, String.class, toString);
Note that it is critical the map be private
, and never returned from any method (don’t put an asMap
method returning a writable Map!! This is to ensure subclasses and clients may not corrupt its internal consistency. Now, the content of the ConverterLibrary
class is still Level Three (any change must be carefully examined for “visually obvious” mistakes), but its clients are Level One:
Function<Fruit,Colour> getColour = ...; converters.put(Fruit.class, Juice.class, getColour); //... Function<Fruit,Colour> getColour = converters.get(Fruit.class, Juice.class);
Both line two and line four will trigger compilation errors.
I just added this blog to my rss reader, excellent stuff. Can’t get enough!