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

时间:2016-02-28 09:18:14

标签: java java-stream covariance contravariance invariance

回顾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
)

现有设计相对于建议设计有哪些优势(可能还有EG决策的原因)?

或者,换句话说,我可能忽略了建议设计的注意事项(例如类型推断困难,并行化约束或特定于还原操作的约束,例如关联性,对未来Java声明站点的预期BiFunction<in T, in U, out R>上的差异,...)?

2 个答案:

答案 0 :(得分:5)

浏览lambda开发历史并隔离&#34; THE&#34;这个决定的原因很难 - 所以最终,人们将不得不等待其中一个开发人员回答这个问题。

一些提示可能如下:

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

  • 有趣的是,很长一段时间,lambda提案包含一个专用接口Combiner<T,U,R>。与直觉相反,这并未用作combiner函数中的Stream#reduce。相反,它被用作reducer,这似乎是现在所谓的accumulator。但是,Combiner界面为replaced with BiFunction in a later revision

  • 这个问题最引人注目的相似之处在于thread about the Stream#flatMap signature at the mailing list,然后将其转化为关于流方法签名的方差的一般问题。他们在某些地方修复了这些问题,例如

      

    布赖恩纠正我:

         

    <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&#39;,但如果&#39; BiFunction&#39;是   然后使用我们有更多的灵活性

         

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

         

    关于&#39; BinaryOperator&#39;

    的相同评论

    (我强调)。

我发现BinaryOperator替换BiFunction的唯一理由最终在response to this statement, in the same thread中给出:

  

即使正如你所说的那样,BinaryOperator也不会被BiFunction取代   它引入了更多的灵活性,一个BinaryOperator问两个参数   和返回类型是相同的,所以它在概念上更重   (EG已经对此投了票)。

也许有人可以挖掘出专家组对该决定的投票的特定参考,但也许这句话已足以回答为什么它就是这样的问题......

答案 1 :(得分:1)

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

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

例如我是你梦寐以求的jOOQ库的用户,也是一个喜欢泛型谜题的好奇的Java开发人员,但是当我必须在我自己的界面中放入通配符时,我经常会想念SQL元组的简单性Result<T>中类型参数的类型以及处理记录类型的接口时产生的麻烦 - 而不是“jOOQ故障”