在 Stream.reduce() 这样的 API 中选择不变性的好理由是什么?

回顾 Java 8 API 设计,我对 Stream.reduce() 参数上的通用不变性感到惊讶:Stream

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

同一 API 的看似更通用的版本可能已对 的单个引用应用了协方差/逆变,例如:U

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

这将允许以下情况,目前这是不可能的:

// Assuming we want to reuse these tools all over the place:
BiFunction<Number, Number, Double> numberAdder =
    (t, u) -> t.doubleValue() + u.doubleValue();

// This currently doesn't work, but would work with the suggestion
Stream<Number> stream = Stream.of(1, 2L, 3.0);
double sum = stream.reduce(0.0, numberAdder, numberAdder);

解决方法是使用方法引用将类型“强制”为目标类型:

double sum = stream.reduce(0.0, numberAdder::apply, numberAdder::apply);

C#没有这个特殊的问题,因为Func(T1,T2,TResult)定义如下,使用声明站点方差,这意味着任何使用API的API都免费获得此行为:Func

public delegate TResult Func<in T1, in T2, out TResult>(
    T1 arg1,
    T2 arg2
)

与建议的设计相比,现有设计的优势(以及EG决策的原因)是什么?

或者,换个角度问,我建议的设计中有哪些警告,我可能会忽略(例如,类型推断困难,并行化约束,或特定于约简操作的约束,例如关联性,对未来Java的声明站点差异的预期,...)?BiFunction<in T, in U, out R>


答案 1

爬过 lambda 开发的历史并隔离此决定的“THE”原因很困难 - 因此最终,人们将不得不等待其中一个开发人员回答这个问题。

一些提示可能如下:

  • 流接口经历了多次迭代和重构。在接口的最早版本之一中,有专用方法,并且最接近问题中的方法在当时仍称为Stream#fold。这个已经收到了 一个作为参数。StreamreducereduceBinaryOperatorcombiner

  • 有趣的是,在相当长的一段时间内,lambda提案包括一个 专用界面 。与直觉相反,这没有用作函数中的。相反,它被用作 ,这似乎是现在所说的 。但是,该接口在后来的修订版中被 BiFunction 取代Combiner<T,U,R>combinerStream#reducereduceraccumulatorCombiner

  • 与此处的问题最惊人的相似之处是在邮件列表中有关 Stream#flatMap 签名的线程中找到,然后将其转换为有关流方法签名差异的一般问题。他们在某些地方修复了这些问题,例如

    正如布莱恩纠正我的那样:

    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

    而不是:

    <R> Stream<R> flatMap(Function<T, Stream<? extends R>> mapper);

    但请注意,在某些地方,这是不可能的:

    T reduce(T identity, BinaryOperator<T> accumulator);

    Optional<T> reduce(BinaryOperator<T> accumulator);

    无法修复,因为他们使用了“BinaryOperator”,但是如果使用“BiFunction”,那么我们有更多的灵活性。

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

    而不是:

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

    关于“二进制操作者”的相同评论

    (由我强调)。


我发现用 a 替换的唯一理由最终在同一线程中给出了对此语句的响应BinaryOperatorBiFunction

BinaryOperator不会被BiFunction取代,即使如你所说,它引入了更大的灵活性,BinaryOperator要求两个参数和返回类型相同,这样它在概念上就具有更大的权重(EG已经对此进行了投票)。

也许有人可以挖掘出指导这一决定的专家组投票的一个具体的参考,但也许这句话已经充分回答了为什么它是这样的问题......


答案 2

在我看来,只是建议的增强功能没有真正的用例。提议的Javadoc还有3个类型参数和5个通配符。我想这足以将整个事情简化为官方API,因为普通的Java开发人员不希望(通常甚至无法)失去理智,试图让编译器满意。只是为了记录,您的类型签名中只有165个字符。reduce()

此外,参数通常以 lambda 表达式的形式提供,因此,当此类表达式通常不包含或非常简单的业务逻辑,因此仅使用一次时,拥有更多通用版本就没有实际意义。.reduce()

例如,我是您出色的jOOQ库的用户,也是一个喜欢泛型谜题的好奇的Java开发人员,但是当我不得不在自己的接口中放置通配符时,我经常会错过SQL元组的简单性,因为Result<T>中的类型参数以及它在处理记录类型接口时产生的麻烦 - 并不是说它是jOOQ错误


推荐