Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inconsistency in Java 8 method signatures

Tags:

java

java-8

Java 8 has given us new methods with really long signatures like this:

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)

What I find odd about this is that wildcards have been used to ensure that the first two parameters are as general as possible, yet the third parameter is just a BinaryOperator<U>. If they'd been consistent, surely it would be a BiFunction<? super U,? super U,? extends U>?. Am I missing something? Is there a good reason for this, or did they just want to avoid making an already horrendous signature even worse?

Edit

I understand PECS, and I understand the principle that mergeFunction should be thought of as a way of taking two Us and getting back a U. However it would be useful to be able to have an object that could be reused in many different ways. For example:

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

Obviously this is not a BinaryOperator<Double>, but it could be treated as one. It would be great if you could use MULTIPLY_DOUBLES as both a BiFunction<Number, Number, Double> and a BinaryOperator<Double>, depending on the context. In particular, you could simply pass MULTIPLY_DOUBLES to indicate that you want a load of doubles to be reduced using multiplication. However the signature for toMap (and other new methods in Java 8) does not allow for this kind of flexibility.

like image 214
Paul Boddington Avatar asked Mar 24 '15 20:03

Paul Boddington


3 Answers

You are right in that the functional signature of the merge operation (the same applies to reduce) does not require an interface like BinaryOperator.

This can not only be illustrated by the fact that the mergeFunction of the toMap collector will end up at Map.merge which accepts a BiFunction<? super V,? super V,? extends V>; you can also convert such a BiFunction to the required 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);

or full generic:

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

The most likely reason for creating BinaryOperator and UnaryOperator is to have symmetry with the primitive type versions of these functions which don’t have such a super interface.

In this regard, the methods are consistent

  • Stream.reduce(BinaryOperator<T>)
  • IntStream.reduce(IntBinaryOperator)
  • DoubleStream.reduce(DoubleBinaryOperator)
  • LongStream.reduce(LongBinaryOperator)

or

  • Arrays.parallelPrefix(T[] array, BinaryOperator<T> op)
  • Arrays.parallelPrefix(int[] array, IntBinaryOperator op)
  • Arrays.parallelPrefix(double[] array, DoubleBinaryOperator op)
  • Arrays.parallelPrefix(long[] array, LongBinaryOperator op)
like image 172
Holger Avatar answered Nov 20 '22 08:11

Holger


the BinaryOperator<U> mergeFunction needs to take Us from an input source and put them into another consumer.

Due to the Get and Put Principle, the type has to be exactly the same. No wild cards.

The get-put principle, as stated in Naftalin and Wadler's fine book on generics, Java Generics and Collections says:

Use an extends wildcard when you only get values out of a structure, use a super wildcard when you only put values into a structure, and don't use a wildcard when you do both.

Therefore it can't beBiFunction<? super U,? super U,? extends U> mergefunction because we are doing get and put operations. Therefore the input and result type must be identical.

see these other links for more about Get and Put:

Explanation of the get-put principle (SO question)

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

EDIT

As Gab points out, the Get and Put principle is also known by the Acronym PECS for "Producer Extends Consumer Super"

What is PECS (Producer Extends Consumer Super)?

like image 30
dkatzel Avatar answered Nov 20 '22 08:11

dkatzel


Looking at the implementation of the Collectors#toMap in question, one can see that the operator is passed through to some other methods, but eventually only arrives as the remappingFunction in various forms of Map#merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction).

So using BiFunction<? super V, ? super V, ? extends V> instead of BinaryOperator<V> would indeed work here, without causing any problem. But not only here: The BinaryOperator is only a specialization of BiFunction for the case that the operands and the result are all of the same type. So there are many places where one could allow passing in a BiFunction<? super V, ? super V, ? extends V> instead of a BinaryOperator<V> (or, more obviously: One could always use a BiFunction<V, V, V> instead...)


So up to this point, there seems to be no technical reason why they chose to only support a BinaryOperator<U>.

There was already speculation about possible non-technical reasons. For example, limiting the complexity of the method signature. I'm not sure whether this applies here, but it could, indeed, be a trade-off between the complexity of the method and the intended application cases: The concept of a "binary operator" is easily comprehensible, for example, by drawing analogies to a simple addition or the union of two sets - or maps, in this case.

A possible not-so-obvious technical reason could be that there should be the possibility to provide implementations of this method that internally would not be able to cope with the BiFunction. But considering that the BinaryOperator is only a specialization, it's hard to imagine what such an implementation should look like.

like image 2
Marco13 Avatar answered Nov 20 '22 06:11

Marco13