37

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?
wassgren
  • 18,651
  • 6
  • 63
  • 77

4 Answers4

45

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.)

Community
  • 1
  • 1
aioobe
  • 413,195
  • 112
  • 811
  • 826
  • I accept the bet. I know the [relevant chapter](http://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html) and I bet it will be *very* hard to sort out why it doesn’t work here, *if* the failure is covered by the specification at all. – Holger Jan 12 '15 at 11:57
  • 1
    I just wanted to say that for a `Function super T,? extends K>` target type the compiler will usually infer the correct types, especially, it will not prefer `Object` for the `? super T` part. Usually. [and that even works when `T` is an unknown type `extends Number`](http://stackoverflow.com/a/27901576/2711488). So the only thing that is open is the “why not with wildcards” question… – Holger Jan 12 '15 at 13:07
  • 1
    Indeed, it compiles with `jdk1.9.0`… – Holger Jan 12 '15 at 13:35
4

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.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • 1
    Interesting find. Just when I thought I had fully grasped the situation. :-/ – aioobe Jan 12 '15 at 12:45
  • 1
    Turns out it's a javac bug. Updated my answer. – aioobe Jan 12 '15 at 13:26
  • Aren't `? extends Number` and `N extends Number` two different things? The latter is used to restrict the possible types for N, whereas `? extend Number` **is** the type. I think your example qualifies as [helper method](http://docs.oracle.com/javase/tutorial/java/generics/capture.html) and is a nice workaround, so +1. – a better oliver Jan 12 '15 at 18:31
  • 1
    @zeroflagL: this kind of helper method is required when you want to modify a collection or, more generally, get a value from an instance of a generic class and pass it back to the same instance. Or when you need to refer to the type explicitly. But normally, you don’t need such workaround for a simple read-only operation as in that context (like here), `? extends X` is indeed equivalent to `ArbitraryTypeParameter extends X`. Both implies an unknown type which has to be assignable to `X`. – Holger Jan 12 '15 at 19:07
  • _" I don’t know if there is a difference between wildcards and other types in this regard"_: you may have seen that JLS explicitly distinguishes between "type" and "wildcard" (in particular in 18.2.3), implying that a "wildcard" is not a "type"!! Just saying. – Stephan Herrmann Sep 08 '16 at 21:42
  • @Stephan Herrmann: of course, a wildcard is not a type. That was sloppy from my side. I should have written: “I don’t know if there is a difference between *wildcard-parameterized* types and other (parametrized) types in this regard” – Holger Sep 09 '16 at 09:30
2

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.

sol4me
  • 15,233
  • 5
  • 34
  • 34
  • 2
    The fact that the compiler assigned the type Object to the number variable is pointed out in the question. I think the question is *why* it's not of type Number. – aioobe Jan 11 '15 at 18:21
  • 1
    Ironically, the code in your explanation doesn’t even compile as `Collector>` is not a valid type for this collector. Due to the value extractor function `number -> number` the compiler knows that the `Collector` must be a `Collector>` as the collector’s type without the need to change anything in the `toMap` invocation as the correctness of the key extractor function can be proved. – Holger Jan 12 '15 at 11:29
  • 1
    Not a real improvement. What’s missing is the explanation why the compiler should infer `Collector>`… – Holger Jan 12 '15 at 11:35
  • @Holger There is a link to JLS in the answer. Is it not enough? – sol4me Jan 12 '15 at 11:43
  • 1
    No, that’s not enough. An explanation and a link are obviously two entirely different things. – Holger Jan 12 '15 at 11:48
-1

You can do:

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

numberList.stream().collect(Collectors.toMap(Number::intValue, Function.identity()));
Rory G
  • 173
  • 1
  • 11