2

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.

David M. Karr
  • 14,317
  • 20
  • 94
  • 199
  • Either assign the stream to a variable first, or add a type hint. Or, in Java 9, use `Map.of`. – Andy Turner Oct 09 '17 at 18:12
  • check my answer in the dupe (which I'm not convinced is a dupe any more; will reopen from an actual computer). There is a Javac flag there which will switch on extended debugging info for the type inference algorithm. This may give a clue as to the difference. – Andy Turner Oct 09 '17 at 18:29
  • Any idea how to get Eclipse to use that flag? – David M. Karr Oct 09 '17 at 18:40
  • @DavidM.Karr You [can't](https://stackoverflow.com/questions/3061654/what-is-the-difference-between-javac-and-the-eclipse-compiler) . Everything you can set is in RunAs -> Run Configuration – Tezra Oct 09 '17 at 19:03

1 Answers1

5

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, e.g.

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, e.g.

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#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, i.e. would even work without the ? extends in the generic signature of the factory methods. But using these wildcards is still recommended for maximal flexibility.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Outstanding explanation. Concerning your guess about the types of the working and failing case, the working case all had the same type for the values, and in the failing case, all the values were instances of subclasses of the map value type. – David M. Karr Oct 10 '17 at 16:02