简体   繁体   中英

Confusing java generics error with Map and Collector

A while ago I had found the following info about a cleaner way to initialize maps with Java 8: http://minborgsjavapot.blogspot.com/2014/12/java-8-initializing-maps-in-smartest-way.html .

Using those guidelines, I had implemented the following class in one application:

public class MapUtils {
    public static <K, V> Map.Entry<K, V> entry(K key, V value) {
        return new AbstractMap.SimpleEntry<>(key, value);
    }

    public static <K, U> Collector<Map.Entry<K, U>, ?, Map<K, U>> entriesToMap() {
        return Collectors.toMap((e) -> e.getKey(), (e) -> e.getValue());
    }

    public static <K, U> Collector<Map.Entry<K, U>, ?, ConcurrentMap<K, U>> entriesToConcurrentMap() {
        return Collectors.toConcurrentMap((e) -> e.getKey(), (e) -> e.getValue());
    }
}

In that application, I had implemented code like this:

public Map<String, ServiceConfig>   serviceConfigs() {
    return Collections.unmodifiableMap(Stream.of(
            entry("ActivateSubscriber", new ServiceConfig().yellowThreshold(90).redThreshold(80)),
            entry("AddAccount", new ServiceConfig().yellowThreshold(90).redThreshold(80).rank(3)),
            ...
            ).
            collect(entriesToMap()));
}

This code is working perfectly fine.

In a different application, I copied the MapUtils class into a package, and I imported that class in one class the same way I did in the other application.

I entered the following to reference this:

        Map<String, USLJsonBase>    serviceRefMap   =
    Collections.unmodifiableMap(Stream.of(
            entry("CoreService", coreService),
            entry("CreditCheckService", creditCheckService),
            entry("PaymentService", paymentService),
            entry("AccountService", accountService),
            entry("OrdercreationService", orderCreationService),
            entry("ProductAndOfferService", productAndOfferService),
            entry("EquipmentService", equipmentService),
            entry("EvergentService", evergentService),
            entry("FraudCheckService", fraudCheckService)
            ).
            collect(entriesToMap()));

On the "collect" call, Eclipse is telling me the following:

The method collect(Collector<? super Map.Entry<String,? extends USLJsonBase>,A,R>) in the type Stream<Map.Entry<String,? extends USLJsonBase>> is not applicable for the arguments (Collector<Map.Entry<Object,Object>,capture#1-of ?,Map<Object,Object>>)

What simple and completely non-obvious change is required to get this to work?

Update :

I thought that adding a type hint might do it, but I don't understand why the usage in the other application did not require this.

I changed the reference to this, which now doesn't give me a compile error:

    Map<String, USLJsonBase>    serviceRefMap   =
    Collections.unmodifiableMap(Stream.<Map.Entry<String, USLJsonBase>>of(
            entry("CoreService", coreService),
            entry("CreditCheckService", creditCheckService),
            entry("PaymentService", paymentService),
            entry("AccountService", accountService),
            entry("OrdercreationService", orderCreationService),
            entry("ProductAndOfferService", productAndOfferService),
            entry("EquipmentService", equipmentService),
            entry("EvergentService", evergentService),
            entry("FraudCheckService", fraudCheckService)
            ).
            collect(entriesToMap()));

Again, why was the type hint required here, but not in the other application? The only difference is that the other application is returning the map from a function, and the new code is assigning the map to a local variable. I also modified it so that instead of storing into a local variable, I'm passing it to another method (which was the original need). That didn't change the need to add the type hint.

The problem is that Stream.of(…).collect(…) is a method invocation chain and the target type is not propagated through such a chain. So when you assign the result to a parameterized Map , these type parameters are considered for the collect invocation (and the nested entriesToMap() invocation), but not for the Stream.of(…) invocation.

So for inferring the type of the stream created via Stream.of(…) , only the type of the arguments are considered. This works great when all arguments have the same type, eg

Map<String,Integer> map = Stream.of(entry("foo", 42), entry("bar", 100))
                                .collect(entriesToMap());

has no problems, but rarely does the desired thing when the arguments have different type, eg

Map<String,Number> map = Stream.of(entry("foo", 42L), entry("bar", 100))
                               .collect(entriesToMap());

fails, because the compiler doesn't infer Number as common type for Long and Integer , but rather something like “ INT#1 extends Number,Comparable<? extends INT#2> INT#1 extends Number,Comparable<? extends INT#2> INT#2 extends Number,Comparable<?>

You didn't post the declarations that would allow us to determine the type of the arguments in your specific case, but I'm quiet sure that this is the difference between your variants, in the first one, either all arguments have the same type or the inferred common super type matches your desired result type exactly, whereas in the second case, the arguments have either, different types or a subtype of the desired result type.

Note that even

Map<String,Number> map = Stream.of(entry("foo", 42), entry("bar", 100))
                               .collect(entriesToMap());

does not work, because the inferred stream type is Stream<Map.Entry<String,Integer>> which your collector does not accept for producing a Map<String,Number> .

This leads to the solution to relax the generic signature of your collector.

public static <K, U>
Collector<Map.Entry<? extends K, ? extends U>, ?, Map<K, U>> entriesToMap() {
    return Collectors.toMap((e) -> e.getKey(), (e) -> e.getValue());
}

This fixes both examples, not only accepting Map.Entry<String,Integer> for a Map<String,Number> , but also accepting the intersection type the compiler inferred as base type of Integer and Long .


But I recommend an alternative, not to let each client repeat the Stream.of(…).collect(…) step at all. Compare with the new factory methods of Java 9 . So the refactored methods inspired by this pattern will look like:

public static <K, V> Map.Entry<K, V> entry(K key, V value) {
    return new AbstractMap.SimpleImmutableEntry<>(key, value);
}

@SafeVarargs
public static <K, V> Map<K,V> mapOf(Map.Entry<? extends K, ? extends V>... entries) {
    return Stream.of(entries)
             .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

@SafeVarargs
public static <K, V> ConcurrentMap<K,V> concurrentMapOf(
                                        Map.Entry<? extends K, ? extends V>... entries) {
    return Stream.of(entries)
             .collect(Collectors.toConcurrentMap(Map.Entry::getKey, Map.Entry::getValue));
}

which can be use much simpler:

Map<String,Integer> map1 = mapOf(entry("foo", 42), entry("bar", 100));
Map<String,Number>  map2 = mapOf(entry("foo", 42), entry("bar", 100));
Map<String,Number>  map3 = mapOf(entry("foo", 42L), entry("bar", 100));

Note that since this usage consist of nested invocations only (no chain), target type inference works throughout the entire expression, ie would even work without the ? extends ? extends in the generic signature of the factory methods. But using these wildcards is still recommended for maximal flexibility.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM