Java 8 Streams: why does Collectors.toMap behave differently for generics with wildcards?
Solution 1
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.)
Solution 2
The declaration of the form List<? extends Number> wildcardList
implies a “list with an unknown type which is Number
or a subclass of Number
”. Interestingly, the same kind of list with unknown type works, if the unknown type is referred by a name:
static <N extends Number> void doTheThingWithoutWildCards(List<N> numberList) {
numberList.stream().collect(Collectors.toMap(
// Here I can invoke "number.intValue()" - the object is treated as a Number
number -> number.intValue(),
number -> number));
}
Here, N
is still “an unknown type being Number
or a subclass of Number
” but you can process the List<N>
as intended. You can assign the List<? extends Number>
to a List<N>
without problems as the constraint that the unknown type extends Number
is compatible.
final List<? extends Number> wildCardList = Arrays.asList(1, 2, 3D);
doTheThingWithoutWildCards(wildCardList); // or:
doTheThingWithoutWildCards(Arrays.asList(1, 2, 3D));
The chapter about Type Inference is not an easy read. I don’t know if there is a difference between wildcards and other types in this regard, but I don’t think that there should be. So its either a compiler bug or a limitation by specification but logically, there is no reason why the wildcard shouldn’t work.
Solution 3
This is due to type inference, In first case you declared List<Number>
so compiler have nothing against when you write number -> Integer.valueOf(number.intValue())
because type of variable number
isjava.lang.Number
But in second case you declared final List<? extends Number> wildCardList
due to which
Collectors.toMap
is translated to something like Collectors.<Object, ?, Map<Object, Number>toMap
E.g.
final List<? extends Number> wildCardList = Arrays.asList(1, 2, 3D);
Collector<Object, ?, Map<Object, Object>> collector = Collectors.toMap(
// Why is number treated as an Object and not a Number?
number -> Integer.valueOf(number.intValue()),
number -> number);
wildCardList.stream().collect(collector);
As a result of which in expression
number -> Integer.valueOf(number.intValue()
type of variable number
is Object and there is no method intValue()
defined in class Object. Hence you get compilation error.
What you need is to pass collector type arguments which helps the compiler to resolve intValue()
error E.g.
final List<? extends Number> wildCardList = Arrays.asList(1, 2, 3D);
Collector<Number, ?, Map<Integer, Number>> collector = Collectors.<Number, Integer, Number>toMap(
// Why is number treated as an Object and not a Number?
Number::intValue,
number -> number);
wildCardList.stream().collect(collector);
Moreover you can use method reference Number::intValue
instead of number -> Integer.valueOf(number.intValue())
For more details on Type Inference in Java 8 please refer here.
wassgren
Experienced agile leader with strong technical background and a sweet spot for new technology. Specialties include technical recruitment, technical mentoring, software development, project management, sales and online marketing. Somewhat fluent in full stack development including UX, information architecture, frontend- and backend development, designing API:s, Continuous Delivery etc. Preferred programming languages and technologies include JavaScript, Node.js, Serverless, AWS, NoSQL such as Redis/Neo4j and a wide variety of frontend- and mobile technologies.
Updated on May 18, 2020Comments
-
wassgren almost 4 years
Assume that you have a
List
of numbers. The values in theList
can be of typeInteger
,Double
etc. When you declare such aList
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 theList
andcollect
it all to aMap
using theCollectors.toMap
(obviously the code below is just an example to illustrate the problem). Lets start off by streaming thenumberList
: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.ObjectFrom the compiler error it is obvious that the
number
in the lambda is treated as anObject
instead of as aNumber
.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 theList
? - Why is the
number
variable in the lambda considered to be anObject
instead of aNumber
?
- When collecting the wildcard version of the