为什么 Stream#reduce 不隐式接受处理超类型元素的累积函数?

2022-09-04 00:45:31

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

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

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

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”。编译器坚持尝试使用作为累积元素来减少流,即使累积函数的参数及其返回类型都存在通用类型。BarFoo

我还发现奇怪的是,只要我显式地将流映射到s流中,我仍然可以采用这种方法:Foo

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

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


答案 1

问题是你肯定在创建一个 - 你必须是,因为你返回一个.如果你改变被声明为返回(同时仍然接受),那么你就没问题了。事实上,返回类型与输入类型相关联,这就是问题所在 - 它不能是协变或逆变,因为它用于输入输出。BinaryOperator<Foo>Foocombined()BarFoo

换句话说 - 你期望返回一个,对吧?因此,这表明您期望调用是 - 这意味着它应该在 上运行。只有 A 具有采用 的单参数方法,并且使用 simple 的 lambda 表达式不是 .reduce((a, b) -> combined(a, b))Optional<Foo>Treduce()FooStream<Foo>Stream<Bar>reduceBinaryOperator<Bar>combinedBinaryOperator<Bar>

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

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

答案 2

我认为这与为什么列表不是列表的原因有关。通过这样做,您可以创建一个流。根据一般规则“如果是一个,那么不是”,它显然不能平凡地转换为流。DerivedBase.map(Bar::new)BarFooBAX<B>X<A>

然后,您正在尝试使用它,但必须创建完全相同类型的流。您想要的是两者的行为,从某种意义上说,您希望它既将流减少到单个实例更改其类型。这更像是一个作业(除了适用于可变类型)。reducereducereducereducemapcollectcollect

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

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

它需要两个功能!为什么?因为 Java 流是可并行化的。因此,流可以按部分执行缩减,然后将部分合并为单个值。在这里这是可能的,因为我们给出了一个额外的.在你的例子中,一个函数应该同时充当累加器和合路器,这造成了关于其签名究竟是什么的所有混乱。combiner

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

因此,它看起来像是一个设计决策,旨在避免由于流在减少后随机更改类型而可能造成的混淆。如果你真的想要它,还有另一个重载,但它的签名足够丑陋,只需看代码就可以使类型更改变得明显。


推荐