Java 8 Streams:为什么 Collectors.toMap 对于带有通配符的泛型会有不同的表现?

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

假设您有 List 个数字。 List 中的值可以是 IntegerDouble 等类型。当您声明这样的 List 时,可以使用通配符 (?) 或没有通配符。

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

所以,现在我想 streamListcollect 上使用 Collectors.toMap 全部变成 Map(显然是下面的代码只是一个例子来说明问题)。让我们从流式传输 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));

但是,我不能在 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));

编译器通过以下消息抱怨对 number.intValue() 的调用:

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

从编译器错误可以明显看出,lambda 中的 number 被视为 Object 而不是 Number

所以,现在回答我的问题:

是类型推断不对。如果您明确提供类型参数,它会按预期工作:

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

这是一个已知的 javac 错误:Inference should not map capture variables to their upper bounds。根据 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

显然还没有推送修复程序。 (感谢 Joel Borggrén-Franck 为我指明了正确的方向。)

这是由于 type inference,在第一种情况下你声明了 List<Number> 所以当你写 number -> Integer.valueOf(number.intValue()) 时编译器没有任何反对,因为变量 number 的类型是 java.lang.Number

但是在第二种情况下你声明了 final List<? extends Number> wildCardList 因为 Collectors.toMap 被翻译成类似 Collectors.<Object, ?, Map<Object, Number>toMap 的形式,例如

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

结果在表达式中

number -> Integer.valueOf(number.intValue()

变量 number 的类型是 Object 并且 class Object 中没有定义方法 intValue()。因此你会得到编译错误。

您需要传递收集器类型参数,这有助于编译器解决 intValue() 错误,例如

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

此外,您可以使用方法参考 Number::intValue 而不是 number -> Integer.valueOf(number.intValue())

有关Java 8 中类型推断的更多详细信息,请参阅here

List<? extends Number> wildcardList 形式的声明意味着“具有未知类型的列表,它是 NumberNumber 的子类”。有趣的是,如果未知类型由名称引用,则具有未知类型的同类列表有效:

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));
}

在这里,N 仍然是“未知类型 NumberNumber 的子类”,但您可以按预期处理 List<N>。您可以毫无问题地将 List<? extends Number> 分配给 List<N>,因为未知类型 extends Number 是兼容的约束。

final List<? extends Number> wildCardList = Arrays.asList(1, 2, 3D);
doTheThingWithoutWildCards(wildCardList); // or:
doTheThingWithoutWildCards(Arrays.asList(1, 2, 3D));

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 或规范的限制,但从逻辑上讲, 没有理由不使用通配符。

你可以这样做:

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

numberList.stream().collect(Collectors.toMap(Number::intValue, Function.identity()));