I need to unite map keys of different Maps
in Java 8. There are two important requirements:
- The maps do all have the same (generified) key type but may have totally different value types.
- The maps are accessed through
Supplier
s. This allows late evaluation, i.e. the map is only evaluated in the method itself, not when the method is called.
Using the Stream API, I code this:
1 2 3 4 5 |
public <Ref> Collection<Ref> uniteKeys(Supplier<Map<Ref,?>... maps) { return Stream.of(maps).map(Supplier::get) .flatMap(x->x.keySet().stream()) .collect(Collectors.toCollection(HashSet::new)); } |
Now, let’s define some data:
1 2 3 4 5 6 |
Supplier<Map<Integer,String> map1supp=()->{ Map<Integer,String> m=new HashMap<>(); m.put(1,„A”); m.put(2,„B”); return m; }; Supplier<Map<Integer,Integer> map2supp=()->{ Map<Integer,Integer> m=new HashMap<>(); m.put(3,9); m.put(4,8); return m; }; |
…and finally try to unite the keys – also as a Supplier
so that evaluation is delayed until the field is actually accessed:
1 |
Supplier<Collection<Integer> unitewildcard=()->uniteKeysWildcard(map1supp,map2supp); |
The problem is: It does not work. Eclipse says:
The method uniteKeys(Supplier<Map<Ref,?>>...) is not applicable for the arguments (Supplier<Map<Integer,String>>, Supplier<Map<Integer,Integer>>)
So, what’s the problem here? I digged through some parts of the internet and finally found a blog article explaining the generic wildcard syntax of Java and the semantics. With the help of this article, I could nail the issue down to a misunderstanding of variance inheritance in Java’s generics system on my behalf. Explaining co-variance, the author Zhong Yu explains:
[W]e cannot use aSupplier<Integer>
where aSupplier<Number>
is expected. That is very counter-intuitive. […]Intuitively co-variant types are almost always used with upper-bounded wildcards, particularly in public APIs. If you see a concrete type
Supplier<Something>
in an API, it is very likely a mistake.
Ok, thank you, Zhong Yu. Having learned this, I redesign my uniteKeys()
method using explicitly declared co-variance with upper-bounded wildcards:
1 2 3 4 5 |
public <Ref> Collection<Ref> uniteKeys(Supplier<? extends Map<Ref,?>... maps) { return Stream.of(maps).map(Supplier::get) .flatMap(x->x.keySet().stream()) .collect(Collectors.toCollection(HashSet::new)); } |
Note that the only change to the first version is the parameter declaration. The method implementation itself is totally unchanged. Result: Now it works flawlessly.
Even ten years after their introduction to the language, Java generics can be tricky. Nevertheless, it is important to understand them, as they have become more important than ever in modern functional Java. The whole thing here is about compilation, not about runtime behaviour – remember that all generic information is erased out of the .class
files during compilation.
So, generics are there to help the compiler ensure that type assignments are correct at compile time. Using type inference, it deduces types from the code and checks that assignments are correct regarding the types. And this can really be tricky. Note that the first, wrongly defined uniteKeys()
version actually can be called like this:
1 2 |
Supplier<Collection<Integer> unitewildcard= ()->uniteKeysWildcard(map1supp::get,map2supp::get); |
If I have understood it correctly, this code „wraps” another Supplier
around the actual mapXsupp
and that Supplier
infers the Map
s values types to the wildcard ?
of the uniteKeys()
definition. Anyway, this version is clearly inferior compared to the correctly defined uniteKeys
as it needs that additional (and totally useless) implicit Supplier
.
Anyway, I think this is a really nice example of the complexities of modern Java.
If someone wants to play with this, I add here a complete example class with the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public class WildcardMapValueSupplier { // Use „static” for everything to keep the demo code short, non-static version behaves the same /** Return set of all keys in all maps returned by the given Suppliers, maps correctly generified */ static <Ref> Collection<Ref> uniteKeysWildcard(Supplier<? extends Map<Ref,?>... maps) { return Stream.of(maps).map(Supplier::get).flatMap(x->x.keySet().stream()).collect(Collectors.toCollection(HashSet::new)); } /** Return set of all keys in all maps returned by the given Suppliers, maps wrongly generified */ static <Ref> Collection<Ref> uniteKeysWildcardWRONG(Supplier<Map<Ref,?>... maps) { return Stream.of(maps).map(Supplier::get).flatMap(x->x.keySet().stream()).collect(Collectors.toCollection(HashSet::new)); } // Some data static final Supplier<Map<Integer,String> map1supp=()->{ Map<Integer,String> m=new HashMap<>(); m.put(1,„A”); m.put(2,„B”); return m; }; static final Supplier<Map<Integer,Integer> map2supp=()->{ Map<Integer,Integer> m=new HashMap<>(); m.put(3,9); m.put(4,8); return m; }; // Works as expected static final Supplier<Collection<Integer> unitekeys=()->uniteKeysWildcard(map1supp,map2supp); // Works also by tricking the type inference through a kind of „supplier indirection”. static final Supplier<Collection<Integer> unitekeysWRONG=()->uniteKeysWildcardWRONG(map1supp::get,map2supp::get); public static void main(String[] args) { System.out.println(„With correct definition: „+unitekeys.get()); // [1, 2, 3, 4] as expected System.out.println(„With circumvention of wrong definition: „+unitekeysWRONG.get()); // [1, 2, 3, 4], too (surprisingly…) } } |
Have fun with it!