【问题标题】:Why doesn't Stream#reduce implicitly accept an accumulative function handling super type elements?为什么 Stream#reduce 不隐式接受处理超类型元素的累积函数?
【发布时间】:2016-05-14 16:03:41
【问题描述】:

考虑这些类和累积函数,它们代表了我原始上下文的简化(但重现了相同的问题):

abstract static class Foo {
    abstract int getK();
}
static class Bar extends Foo {
    int k;
    Bar(int k) { this.k = k; }
    int getK() { return this.k; }
}

private static Foo combined(Foo a1, Foo a2) {
    return new Bar(a1.getK() + a2.getK());
}

我试图通过依赖单独的函数 combined 来执行项目的累积(最初是数据索引报告),该函数直接处理 Foo 类型的元素。

Foo outcome = Stream.of(1,2,3,4,5)
        .map(Bar::new)
        .reduce((a,b) -> combined(a, b))
        .get();

事实证明,这段代码导致编译错误(OpenJDK“1.8.0_92”):“lambda 表达式中的返回类型错误:Foo 无法转换为 Bar”。编译器坚持尝试使用 Bar 作为累积元素来减少流,即使 Foo 作为累积函数的参数及其返回类型的公共类型也是如此。

我还发现,只要我明确地将流映射到Foos 的流,我仍然可以采用这种方法:

Foo outcome = Stream.of(1,2,3,4,5)
        .<Foo>map(Bar::new)
        .reduce((a,b) -> combined(a, b))
        .get();

这是 Java 8 泛型类型推断的限制,Stream#reduce 的这种特殊重载的小问题,还是 Java 规范支持的有意行为?我已经阅读了其他一些关于类型推断“失败”的问题,但是这个特殊情况对我来说仍然有点难以理解。

【问题讨论】:

  • 从赋值中推断的类型与从map 的参数推断的类型之间存在冲突,以确定流的类型。我认为 Java 会采用最具体的方式。

标签: java java-8 java-stream type-inference


【解决方案1】:

问题是你肯定是在创建一个BinaryOperator&lt;Foo&gt; - 你必须是,因为你要返回一个Foo。如果您将combined() 更改为声明返回Bar(同时仍接受 Foo),那么您会没事的。问题在于返回类型与输入类型相关联 - 它既不能是协变的也不能是逆变的,因为它用于输入输出。

换一种说法 - 你期待reduce((a, b) -&gt; combined(a, b)) 返回一个Optional&lt;Foo&gt;,对吧?这表明您期望reduce() 调用的TFoo - 这意味着它应该在Stream&lt;Foo&gt; 上运行。 Stream&lt;Bar&gt; 只有一个单参数 reduce 方法,它采用 BinaryOperator&lt;Bar&gt;,而您使用 combined 简单的 lambda 表达式不是 BinaryOperator&lt;Bar&gt;

另一种选择是向 lambda 表达式添加强制转换:

Foo outcome = Stream.of(1,2,3,4,5)
    .map(Bar::new)                
    .reduce((a,b) -> (Bar)combined(a, b))
    .get();

【讨论】:

  • 我知道更改返回类型也可以。但在这种情况下,我希望处理Foo's。主要关注的问题是为什么reduce 在使用Stream&lt;T&gt; where T extends Foo 时首先不能接受Foo 的二元运算符。
  • @E_net4:因为它接受BinaryOperator&lt;T&gt;,而您的lambda表达式不能按原样转换为BinaryOperator&lt;Bar&gt;
  • @E_net4:简而言之,我认为你最终只能接受它。例如,如果 reduce 被设计为接受 BiFunction&lt;T, T, R&gt;,这可能会有所不同。
  • 哈,所以这确实可以与reduce 中的设计更改一起使用,不是吗?这一点与我的问题有关。 :)
  • @E_net4:想多了,问题是你需要在减少之间保持T。例如,看看其他重载,并考虑 reduce 必须做什么。 (我认为我说它只需要一个 BiFunction 是错误的。它可能需要一个 BinaryOperator&lt;U&gt;U super T,但我现在无法检查。)
【解决方案2】:

我认为这与Derived 的列表不是Base 的列表的原因有关。通过执行.map(Bar::new),您可以创建Bar 的流。根据一般规则“如果BA,那么X&lt;B&gt; 不是 X&lt;A&gt;”,它显然不能轻易转换为Foo 流。

然后您尝试reduce 它,但reduce 必须创建完全相同类型的流。你想要的是reduce 表现得像reducemap,因为你希望它都将流减少到单个实例改变它的类型。这更像是 collect 的工作(除了 collect 使用可变类型)。

但是reduce 有一个变体可以改变类型。而它的签名实际上给了我们一个提示,为什么简单的重载不能改变流的类型:

<U> U reduce(U identity,
             BiFunction<U,? super T,U> accumulator,
             BinaryOperator<U> combiner)

它需要两个函数!为什么?因为 Java 流是可并行的。因此,流可以按部分执行归约,然后将部分组合成一个值。在这里它是可能的,因为我们提供了额外的combiner。在您的情况下,一个函数应该同时充当累加器和组合器,这会造成关于其签名到底是什么的所有混淆。

所以它不起作用的原因是因为重载缺少可以组合部分结果的组合器。当然,这不是一个“硬”的原因,因为FooBar 的超类,所以技术上可以使用与累加器和组合器相同的东西,就像您在示例中使用显式&lt;Foo&gt;.

因此,它看起来像是一个设计决策,旨在避免由于归约后流随机更改类型而可能造成的混淆。如果你真的想要它,还有另一个重载,但它的签名丑陋到足以使类型更改仅通过查看代码就显而易见。

【讨论】:

    【解决方案3】:

    更新

    您的问题的简单答案是您的reduce 接受BinaryOperator&lt;Bar&gt;,这表示您必须传入一个接受两个Bar 值并返回Bar 的函数。你的 lambda 接受两个 Bar 参数就好了,因为你的 combined 函数看到 Bar 可以分配给 Foo。然而,对于返回类型,想法却是相反的;而对于参数,您担心传入的值与您的函数匹配,对于返回类型,您必须担心函数的调用者是否可以接受函数返回的类型。因为您的函数返回Foo,而接收者期待Bar,所以存在编译器错误,原因与此失败的原因相同:

    Bar b = new Foo(1);
    

    添加演员表有效,但不安全,原因与

    Bar b  = (Bar) new Foo(1);
    

    有效但不安全——并非Foo 的每个实例都必然是Bar

    使用 3 参数 reduce 可以安全地进行归约,无需强制转换。它可以让您减少到与流类型不同的类型。

    Foo combined = Stream.of(1,2,3,4,5)
                .map(Bar::new)
                .reduce(
                    new Bar(0),                       // starting value, type Foo
                    (Foo a, Bar b) -> combined(a, b), // apply each Bar to create new Foo 
                    (Foo a, Foo b) -> combined(a,b)); // combine intermediate results if parallel
    

    第一个参数(Foo)new Bar(0)是种子减少的初始值,如果流为空,则返回值。

    第二个参数将每个BarStream 折叠成一个新的累积Foo 值。

    第三个参数,主要用于并行流,结合了两个中间累积的Foo 值。

    combine 函数对累加器和合并器都有效,只是因为 Bar 恰好可以分配给 Foo

    请注意,正如 cmets 中提到的,这可以写得更简洁,因为在这种情况下,Stream 元素类型可以分配给 Accumulator 类型,因此可以简化为

    Foo combined = Stream.of(1,2,3,4,5)
                .map(Bar::new)
                .reduce(new Bar(0), (a,b) -> combined(a, b), (a, b) -> combined(a,b));
    

    或者

    Foo combined = Stream.of(1,2,3,4,5)
                .map(Bar::new)
                .reduce(new Bar(0), MyClass::combined, MyClass::combined);
    

    【讨论】:

    • 您不需要将 Bar 转换为 Foo。 U 应该从分配的类型中推断出来
    • 谢谢,@njzk2。我试图显示需要哪些类型,而new Bar(0) 本身似乎对我有误导性——Foo 是必需的,并非每种情况都有一个可分配给 T 的 U。不过我会更新我的答案。
    【解决方案4】:

    您确实遇到了 Java 8 类型推断的限制。一句话,目标类型不适用于方法调用的接收者。因此,在您的代码中,您对Stream.of(1,2,3,4,5).map(Bar::new) 的结果调用了方法reduce,这使得该表达式成为方法调用.reduce((a,b) -&gt; combined(a, b)) 的接收者,它不能使用任何关于reduce 可能期望的信息.

    相反,.map 调用的接收者表达式 Stream.of(1,2,3,4,5) 的类型和参数 Bar::new 是确定结果流类型的唯一信息,没有任何迹象表明 @与Stream&lt;Bar&gt; 相比,987654330@ 可能是更好的选择。

    这与this answerthat answer 中描述的问题相同。

    如果可以进行类型推断,可以通过分解方法调用来轻松演示它是如何工作的:

    Stream<Foo> s= Stream.of(1,2,3,4,5).map(Bar::new);
    Foo outcome = s.reduce((a,b) -> combined(a, b)).get();
    

    运行顺畅。

    除了像 .&lt;Foo&gt;map(Bar::new) 一样为 map 提供显式类型之外,还有另一种选择

    Optional<Foo> outcome =
        Stream.of(1,2,3,4,5)
        .map(Bar::new)
        .collect(Collectors.reducing((a,b) -> combined(a, b)));
    

    这是因为Stream&lt;T&gt; 上的collect 允许Collector&lt;? super T,…&gt; 类型的参数,因此编译器可以从预期的目标类型推断Collector&lt;Foo,…&gt;,这对于在Stream&lt;Bar&gt; 上调用collect 是有效的。但是,出于与上述相同的原因,您不能立即链接 get() 调用以获取 Foo;目标类型不会提供给方法调用的接收者。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-06-12
      • 1970-01-01
      • 1970-01-01
      • 2016-01-27
      • 1970-01-01
      • 1970-01-01
      • 2018-01-02
      • 1970-01-01
      相关资源
      最近更新 更多