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 U
s 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 double
s 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.
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)
the BinaryOperator<U> mergeFunction
needs to take U
s 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)?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With