【问题标题】:Java 8 Streams: why does Collectors.toMap behave differently for generics with wildcards?Java 8 Streams:为什么 Collectors.toMap 对于带有通配符的泛型表现不同?
【发布时间】:2015-03-09 12:37:17
【问题描述】:

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

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

所以,现在我想使用Collectors.toMapstream 超过Listcollect 全部转换为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:找不到符号
符号:方法 intValue()
位置:java.lang.Object类型的变量号

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

那么,现在来回答我的问题:

  • 在收集List 的通配符版本时,为什么它不像List 的非通配符版本那样工作?
  • 为什么 lambda 中的 number 变量被认为是 Object 而不是 Number

【问题讨论】:

    标签: java generics lambda java-8 collectors


    【解决方案1】:

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

    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 的说法,该状态

    尝试了一个修复然后退出,因为它在 8 中破坏了案例,所以我们在 8 中进行了更保守的修复,同时在 9 中完成了全部工作

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

    【讨论】:

    • 我接受这个赌注。我知道relevant chapter,并且我敢打赌,非常很难弄清楚为什么它在这里不起作用,如果规范完全涵盖了故障。
    • 我只是想说,对于Function&lt;? super T,? extends K&gt; 目标类型,编译器通常会推断出正确的类型,尤其是,它不会更喜欢Object 用于? super T 部分。通常。 and that even works when T is an unknown type extends Number。所以唯一开放的是“为什么不使用通配符”的问题……
    • 确实,它用jdk1.9.0编译...
    【解决方案2】:

    List&lt;? extends Number&gt; 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&lt;N&gt;。您可以毫无问题地将List&lt;? extends Number&gt; 分配给List&lt;N&gt;,因为未知类型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 不易阅读。我不知道通配符和其他类型在这方面是否有区别,但我认为不应该有。所以它的 eithercompiler bug 或者是规范的限制,但从逻辑上讲, 通配符没有理由不工作。

    【讨论】:

    • 有趣的发现。就在我以为自己已经完全掌握情况的时候。 :-/
    • 原来是 javac 错误。更新了我的答案。
    • ? extends NumberN extends Number 不是两个不同的东西吗?后者用于限制 N 的可能类型,而 ? extend Number 类型。我认为您的示例符合helper method 的条件,并且是一个很好的解决方法,所以+1。
    • @zeroflagL:当您想要修改集合或更一般地,从泛型类的实例中获取值并将其传递回同一个实例时,需要这种帮助方法。或者当您需要显式引用类型时。但通常情况下,对于简单的只读操作,您不需要这种解决方法,因为在这种情况下(如这里),? extends X 确实等同于ArbitraryTypeParameter extends X。两者都意味着必须分配给X 的未知类型。
    • "不知道通配符和其他类型在这方面有没有区别":你可能已经看到JLS明确区分了"type"和"通配符”(特别是在 18.2.3 中),暗示“通配符”不是“类型”!!只是说。
    【解决方案3】:

    这是由于type inference,在第一种情况下,您声明了List&lt;Number&gt;,因此当您编写number -&gt; Integer.valueOf(number.intValue()) 时,编译器没有任何反对意见,因为变量number 的类型是java.lang.Number

    但在第二种情况下,您声明了final List&lt;? extends Number&gt; wildCardList,因此 Collectors.toMap 被翻译成 Collectors.&lt;Object, ?, Map&lt;Object, Number&gt;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 -&gt; Integer.valueOf(number.intValue()

    变量number 的类型是Object,并且在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 -&gt; Integer.valueOf(number.intValue())

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

    【讨论】:

    • 问题中指出了编译器将类型 Object 分配给 number 变量的事实。我认为问题是为什么它不是数字类型。
    • 具有讽刺意味的是,您解释中的代码甚至无法编译,因为 Collector&lt;Object, ?, Map&lt;Object, Number&gt;&gt; 不是此收集器的有效类型。由于值提取器函数number -&gt; number,编译器知道Collector 必须是Collector&lt;Number, …。并且由于T 类型参数必须是Number,它将接受Collector&lt;Number, ?, Map&lt;Integer, Number&gt;&gt; 作为收集器的类型,而无需更改toMap 调用中的任何内容,因为可以证明密钥提取函数的正确性。跨度>
    • 不是真正的改进。缺少的是解释为什么编译器应该推断Collector&lt;Object, ?, Map&lt;Object, Object&gt;&gt;...
    • @Holger 答案中有指向 JLS 的链接。还不够吗?
    • 不,这还不够。解释和链接显然是两个完全不同的东西。
    【解决方案4】:

    你可以这样做:

    final List<Number> numberList = Arrays.asList(1, 2, 3D, 4D);
    
    numberList.stream().collect(Collectors.toMap(Number::intValue, Function.identity()));
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多