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



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

<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)定义如下,使用声明站点差异,这意味着任何使用 Func 的 API 都免费获得此行为:

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

与建议的设计相比,现有设计的优势(以及可能的影响因素的原因)是什么?

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

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

一些提示可能如下:

  • 流接口经历了多次迭代和重构。在最早的Stream接口版本之一中,已经有专门的reduce方法,而最接近问题中reduce方法的方法在当时还被称为Stream#fold。这个已经收到了一个BinaryOperator作为combiner参数。

  • 有趣的是,在相当长的一段时间内,lambda提案包括一个专用的接口Combiner<T,U,R> 。与直觉相反,这没有用作Stream#reduce函数中的combiner。相反,它被用作reducer,这似乎是现在所说的accumulator。但是,Combiner 接口在后来的修订版中被替换为 BiFunction

  • 与这里的问题最惊人的相似之处是在邮件列表中关于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);

    无法修复,因为他们使用了"二进制运算符",但如果"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);

    关于"二进制运算符"的相同评论

    (由我强调)。


我发现的不用BiFunction替换BinaryOperator的唯一理由最终在同一线程中对该声明的响应中给出:

BinaryOperator不会被BiFunction取代,即使,正如你所说, 它引入了更多的灵活性,二进制运算符要求两个参数 并且返回类型相同,因此它在概念上具有更大的权重 (专家组已经对此进行了投票)。

也许

有人可以挖掘出管理这一决定的专家组投票的参考资料,但也许这句话已经充分回答了为什么会这样的问题......

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

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

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

相关内容

  • 没有找到相关文章

最新更新