【问题标题】:Inconsistency in Java 8 method signaturesJava 8 方法签名中的不一致
【发布时间】:2015-03-24 20:31:02
【问题描述】:

Java 8 为我们提供了具有非常长签名的新方法,如下所示:

static <T,K,U,M extends Map<K,U>> Collector<T,?,M> toMap(
    Function<? super T,? extends K> keyMapper, 
    Function<? super T,? extends U> valueMapper, 
    BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier)

我觉得奇怪的是通配符被用来确保前两个参数尽可能通用,而第三个参数只是一个BinaryOperator&lt;U&gt;。如果他们一直保持一致,那肯定是BiFunction&lt;? super U,? super U,? extends U&gt;?。我错过了什么吗?这样做有充分的理由吗,还是他们只是想避免让已经很可怕的签名变得更糟?

编辑

我了解 PECS,并且我了解 mergeFunction 应该被认为是获取两个 Us 并取回 U 的一种方式。然而,能够拥有一个可以以多种不同方式重用的对象将会很有用。例如:

static final BiFunction<Number, Number, Double> 
        MULTIPLY_DOUBLES = (a, b) -> a.doubleValue() * b.doubleValue();

显然这不是BinaryOperator&lt;Double&gt;,但可以将其视为。如果您可以将MULTIPLY_DOUBLES 用作both BiFunction&lt;Number, Number, Double&gt;BinaryOperator&lt;Double&gt;,那就太好了,具体取决于上下文。特别是,您可以简单地传递 MULTIPLY_DOUBLES 以表明您希望使用乘法来减少 doubles 的负载。然而,toMap(以及 Java 8 中的其他新方法)的签名不允许这种灵活性。

【问题讨论】:

  • ? extends U 不等于 U
  • @pbabcdefp *Operator原理是对相同的输入和输出参数进行操作;因此supering 参数和extendsing 返回值没有意义。
  • @robbmj 带有类型推断和您没有注意到的 IDE。还不错
  • 不,你的解释是对的。如果它是更广泛的BiFunction,这将起作用,但他们确实希望最小化签名的复杂性——更重要的是,更窄的签名使类型推断更容易工作,所以你可以只传递一个无类型的 lambda 并拥有它推断正确。
  • 具有讽刺意味的是,Map.merge 其中mergeFunction 最终会 接受BiFunction&lt;? super V,? super V,? extends V&gt;

标签: java java-8


【解决方案1】:

你是对的,merge 操作的功能签名(同样适用于 reduce)不需要像 BinaryOperator 这样的接口。

这不仅可以通过toMap 收集器的mergeFunctionMap.merge 结束这一事实来说明,Map.merge 接受BiFunction&lt;? super V,? super V,? extends V&gt;;您也可以将这样的BiFunction 转换为所需的BinaryOperator

BiFunction<Number, Number, Double> 
    MULTIPLY_DOUBLES = (a, b) -> a.doubleValue() * b.doubleValue();
Stream<Double> s = Stream.of(42.0, 0.815);
Optional<Double> n=s.reduce(MULTIPLY_DOUBLES::apply);

或完全通用:

public static <T> Optional<T> reduce(
    Stream<T> s, BiFunction<? super T, ? super T, ? extends T> f) {
    return s.reduce(f::apply);
}

创建BinaryOperatorUnaryOperator 最可能的原因是为了与这些没有超级接口的函数的原始类型版本对称。

在这方面,方法一致的

  • Stream.reduce(BinaryOperator&lt;T&gt;)
  • IntStream.reduce(IntBinaryOperator)
  • DoubleStream.reduce(DoubleBinaryOperator)
  • LongStream.reduce(LongBinaryOperator)

  • Arrays.parallelPrefix(T[] array, BinaryOperator&lt;T&gt; op)
  • Arrays.parallelPrefix(int[] array, IntBinaryOperator op)
  • Arrays.parallelPrefix(double[] array, DoubleBinaryOperator op)
  • Arrays.parallelPrefix(long[] array, LongBinaryOperator op)

【讨论】:

    【解决方案2】:

    BinaryOperator&lt;U&gt; mergeFunction 需要从输入源获取Us 并将它们放入另一个消费者。

    由于 Get 和 Put 原则,类型必须完全相同。没有通配符。

    get-put 原则,如Naftalin and Wadler's fine book on generics, Java Generics and Collections 所述:

    当您只从结构中获取值时使用扩展通配符,当您只将值放入结构时使用超级通配符,当您同时执行这两种操作时不要使用通配符。

    因此它不能是BiFunction&lt;? super U,? super U,? extends U&gt; mergefunction,因为我们正在进行getput 操作。因此输入和结果类型必须相同。

    有关 Get 和 Put 的更多信息,请参阅这些其他链接:

    Explanation of the get-put principle(所以问题)

    http://www.ibm.com/developerworks/library/j-jtp07018/

    编辑

    正如 Gab 所指出的,Get 和 Put 原则也被称为“生产者扩展消费者超级”的首字母缩写词 PECS

    What is PECS (Producer Extends Consumer Super)?

    【讨论】:

    • @Gab 谢谢!忘了那个缩写了。更新答案
    【解决方案3】:

    查看有问题的Collectors#toMap 的实现,可以看到运算符被传递给其他一些方法,但最终仅以Map#merge(K key, V value, BiFunction&lt;? super V,? super V,? extends V&gt; remappingFunction) 的各种形式到达remappingFunction

    所以使用BiFunction&lt;? super V, ? super V, ? extends V&gt; 而不是BinaryOperator&lt;V&gt; 确实可以在这里工作,不会造成任何问题。但不仅在这里:BinaryOperator 只是BiFunction 的特化,用于操作数和结果都是相同类型的情况。所以有很多地方可以允许传入BiFunction&lt;? super V, ? super V, ? extends V&gt;而不是BinaryOperator&lt;V&gt;(或者,更明显的是:可以总是使用BiFunction&lt;V, V, V&gt;代替...)


    所以到目前为止,他们选择只支持BinaryOperator&lt;U&gt; 似乎没有技术原因。

    已经有关于可能的非技术原因的猜测。例如,限制方法签名的复杂性。我不确定这是否适用于这里,但它确实可能是方法的复杂性和预期的应用案例之间的权衡:“二元运算符”的概念很容易理解,例如,通过绘制在这种情况下,类似于简单的 addition 或两个集合的并集 - 或 maps

    一个可能不那么明显的技术原因可能是应该有可能提供此方法的实现,而该方法在内部无法处理BiFunction。但是考虑到BinaryOperator 只是一个专业化,很难想象这样的实现应该是什么样子。

    【讨论】:

    • @Holger 您指的是什么过时的附加接口? BinaryOperator?
    • @pbabcdefp:是的,如果每次使用BinaryOperator&lt;X&gt; 时都使用BiFunction&lt;X,X,X&gt;BiFunction&lt;? super X,? super X,? extends X&gt;,那么就不需要interface
    • @Holger 与UnaryOperator 相同。这 2 个额外的接口实际上降低了灵活性
    • @Holger。原始版本的对称性是我见过的最有说服力的解释。如果你把它作为一个答案,我会接受它。
    • @pbabcdefp 我不想在这里争论或讨价还价 ;-) 严格来说,ALL 功能接口可以作为单个接口提供:Function&lt;A,B&gt;。就是这样(A 类似于Tuple,对于Bi- 和更高的arity 函数)。但是设计者做出了一些合理的决定,哪些功能应该以“固定”形式提供(如BinaryOperator),哪些功能应该存在专用的原始版本。这可能并不容易,有些人可能会抱怨,例如关于缺少Float-primitive 版本等,但这是另一回事......
    猜你喜欢
    • 2019-02-06
    • 2018-01-28
    • 1970-01-01
    • 2012-04-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多