3

While trying to write some Generic code I faced an issue. Of course, I found some workarounds, but still, why the code below is not working?

private static <K, U, M extends Map<K, U>>
Supplier<M> mapSupplier() {
    return  HashMap::new;
}

This returns

Error:(25, 17) java: incompatible types: bad return type in lambda expression
    no instance(s) of type variable(s) K,V exist so that java.util.HashMap<K,V> conforms to M

Update: This map supplier is needed for me to create custom map collector:

public static <E, K, V, M extends Map<K, V>>
Collector<E, ?, M> collectToHashMapWithRandomMerge(BiConsumer<M, E> accumulator) {
    return Collector.of(mapSupplier(),
            accumulator,
            TjiCollectionUtils.randomMapMerger());
}

Calling Collector.of with HashMap::new also causes the same compilation error

Ideally, I don't want to create additional method params and just use the following:

  public static <E, K, V, M extends Map<K, V>>
Collector<E, ?, M> collectToHashMapWithRandomMerge(BiConsumer<M, E> accumulator) {
    return Collector.of(HashMap::new,
            accumulator,
            TjiCollectionUtils.randomMapMerger());
}

Answer I ended up making:

public static <E, K, V>
Collector<E, ?, Map<K, V>> collectToMapWithRandomMerge(BiConsumer<Map<K, V>, E> accumulator) {
    return Collector.of(HashMap::new,
            accumulator,
            TjiCollectionUtils.randomMapMerger());
}

And it's called in the following way:

MyCollectionUtils.collectToMapWithRandomMerge(
    (Map<String,Integer> m, SomeClassToExtractFrom e) -> ...);
Seventh
  • 107
  • 5

2 Answers2

3

This seems to be a common misconception of generics, which is that the callee decides what types the generic parameters are. In fact, the caller does.

Whoever calls mapSupplier gets to decide what K, U and M are. Let's say that I am calling it, and I want K to be Integer, U to be String, and M to be Hashtable<Integer, String>. This is valid because Hashtable implements Map.

Supplier<Hashtable<Integer, String>> htSupplier = mapSupplier();
Hashtable<Integer, String> ht = htSupplier.get();

As the caller, I would expect the above to work, but htSupplier.get, with your implementation, would actually give me a HashMap<Integer, String>, which is an unrelated type to Hashtable (in terms of inheritance hierarchy).

In other words, mapSupplier has single-handedly decided that M should be HashMap<K, U> while also saying that it will work on any M that implements Map<K, U>.

Whenever you see yourself writing a "generic" method that decides what its generic parameters are, that method probably shouldn't have that generic parameter. Therefore, mapSupplier should be probably be rewritten without the M parameter:

private static <K, U>
Supplier<HashMap<K, U>> mapSupplier() {
    return  HashMap::new;
}

EDIT:

Seeing the caller, I think you can either:

  • remove M from collectToHashMapWithRandomMerge as well, or:
  • make collectToHashMapWithRandomMerge accept a Supplier<M>, so that its caller can decide the type of map.
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • 1
    @Sweeper, answer please, can it be written as `private static Supplier extends HashMap> mapSupplier() { return HashMap::new; }` ? – Eugene Kortov Feb 17 '20 at 14:16
2

M extends Map<K, V> means a specific class that extends Map. HashMap is a class that extends Map, but M could be, say, LinkedHashMap; a HashMap isn't a LinkedHashMap.

You appear already to be given a Supplier<M>, as a parameter:

public static <E, K, V, M extends Map<K, V>>
Collector<E, ?, M> collectToHashMapWithRandomMerge(Supplier<M> mapSupplier /* HERE */, BiConsumer<M, E> accumulator) {
    return Collector.of(mapSupplier(),
            accumulator,
            TjiCollectionUtils.randomMapMerger());
}

So, just use mapSupplier (the parameter) instead of mapSupplier() (the result of invoking the method).

Andy Turner
  • 137,514
  • 11
  • 162
  • 243
  • Thanks for the answer! It was just a typo: obviously, I don't want for the method to have extra parameter =) – Seventh Feb 17 '20 at 14:17