Java 8 Streams: why does Collectors.toMap behave differently for generics with wildcards?

wassgren picture wassgren · Jan 11, 2015 · Viewed 23.4k times · Source

Assume that you have a List of numbers. The values in the List can be of type Integer, Double etc. When you declare such a List it is possible to declare it using a wildcard (?) or without a wildcard.

final List<Number> numberList = Arrays.asList(1, 2, 3D);
final List<? extends Number> wildcardList = Arrays.asList(1, 2, 3D);

So, now I want to stream over the List and collect it all to a Map using the Collectors.toMap (obviously the code below is just an example to illustrate the problem). Lets start off by streaming the numberList:

final List<Number> numberList = Arrays.asList(1, 2, 3D, 4D);

numberList.stream().collect(Collectors.toMap(
        // Here I can invoke "number.intValue()" - the object ("number") is treated as a Number
        number -> Integer.valueOf(number.intValue()),
        number -> number));

But, I can not do the same operation on the wildcardList:

final List<? extends Number> wildCardList = Arrays.asList(1, 2, 3D);
wildCardList.stream().collect(Collectors.toMap(
        // Why is "number" treated as an Object and not a Number?
        number -> Integer.valueOf(number.intValue()),
        number -> number));

The compiler complains on the call to number.intValue() with the following message:

Test.java: cannot find symbol
symbol: method intValue()
location: variable number of type java.lang.Object

From the compiler error it is obvious that the number in the lambda is treated as an Object instead of as a Number.

So, now to my question(s):

  • When collecting the wildcard version of the List, why is it not working like the non-wildcard version of the List?
  • Why is the number variable in the lambda considered to be an Object instead of a Number?

Answer

aioobe picture aioobe · Jan 11, 2015

It's the type inference that doesn't get it right. If you provide the type argument explicitly it works as expected:

List<? extends Number> wildCardList = Arrays.asList(1, 2, 3D);
wildCardList.stream().collect(Collectors.<Number, Integer, Number>toMap(
                                  number -> Integer.valueOf(number.intValue()),
                                  number -> number));

This is a known javac bug: Inference should not map capture variables to their upper bounds. The status, according to Maurizio Cimadamore,

a fix was attempted then backed out as it was breaking cases in 8, so we went for a more conservative fix in 8 while doing the full thing in 9

Apparently the fix has not yet been pushed. (Thanks to Joel Borggrén-Franck for pointing me in the right direction.)