Несоответствие в сигнатурах метода Java 8
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<U>
. Если бы они были согласованы, то это было бы BiFunction<? super U,? super U,? extends U>
?. Я что-то упускаю? Есть ли веская причина для этого, или они просто хотят избежать еще более ужасной подписи?
Edit
Я понимаю PECS, и я понимаю принцип, что mergeFunction
следует рассматривать как способ взять два U
и вернуть a U
. Однако было бы полезно иметь объект, который можно было бы повторно использовать разными способами. Например:
static final BiFunction<Number, Number, Double>
MULTIPLY_DOUBLES = (a, b) -> a.doubleValue() * b.doubleValue();
Очевидно, что это не BinaryOperator<Double>
, но его можно рассматривать как единое целое. Было бы здорово, если бы вы использовали MULTIPLY_DOUBLES
как a BiFunction<Number, Number, Double>
, так и BinaryOperator<Double>
, в зависимости от контекста. В частности, вы можете просто передать MULTIPLY_DOUBLES
, чтобы указать, что вы хотите уменьшить нагрузку double
с помощью умножения. Однако подпись для toMap
(и других новых методов в Java 8) не допускает такой гибкости.
Ответы
Ответ 1
Вы правы в том, что функциональная подпись операции слияния (то же самое относится к сокращению) не требует интерфейса типа BinaryOperator
.
Это можно не только проиллюстрировать тем фактом, что mergeFunction
коллектора toMap
окажется в Map.merge
, который принимает значение BiFunction<? super V,? super V,? extends V>
; вы также можете преобразовать такой 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);
}
Наиболее вероятной причиной создания BinaryOperator
и UnaryOperator
является симметрия с примитивными типами этих функций, которые не имеют такого супер-интерфейса.
В этом отношении методы согласованы
-
Stream.reduce(BinaryOperator<T>)
-
IntStream.reduce(IntBinaryOperator)
-
DoubleStream.reduce(DoubleBinaryOperator)
-
LongStream.reduce(LongBinaryOperator)
или
-
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)
Ответ 2
BinaryOperator<U> mergeFunction
необходимо взять U
из источника ввода и поместить их в другого пользователя.
Из-за принципа Get и Put, тип должен быть точно таким же. Никаких диких карт.
Принцип get-put, как указано в Нафталин и книга Вадлера о дженериках, Java Generics и Collections гласят:
Используйте расширенный подстановочный знак, когда вы получаете только значения из структуры, используйте суперсимвольный символ, когда вы только ставите значения в структуру и не используете подстановочный знак, когда делаете то и другое.
Поэтому он не может быть BiFunction<? super U,? super U,? extends U> mergefunction
, потому что мы делаем операции get
и put
. Поэтому тип ввода и результата должен быть идентичным.
см. эти другие ссылки для получения дополнительных сведений о Get и Put:
Объяснение принципа get-put (вопрос SO)
http://www.ibm.com/developerworks/library/j-jtp07018/
ИЗМЕНИТЬ
Как отмечает Габ, принцип Get and Put также известен под сокращением PECS для "Producer Extends Consumer Super"
Что такое PECS (продюсер продлевает потребительский супер)?
Ответ 3
Рассматривая реализацию Collectors # toMap, можно видеть, что оператор передается другим методам, но в конечном итоге только приходит как remappingFunction
в различных формах Map#merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)
.
Таким образом, использование BiFunction<? super V, ? super V, ? extends V>
вместо BinaryOperator<V>
действительно будет работать здесь, не создавая никаких проблем. Но не только здесь: BinaryOperator
- это только специализация BiFunction
для случая, когда операнды и результат имеют одинаковый тип. Таким образом, существует много мест, где можно разрешить передачу в BiFunction<? super V, ? super V, ? extends V>
вместо BinaryOperator<V>
(или, что более очевидно: можно всегда использовать BiFunction<V, V, V>
вместо...)
Таким образом, до сих пор нет технической причины, по которой они решили поддерживать только BinaryOperator<U>
.
Уже были предположения о возможных нетехнических причинах. Например, ограничение сложности сигнатуры метода. Я не уверен, применимо ли это здесь, но может быть, действительно, быть компромиссом между сложностью метода и предполагаемыми случаями приложения. Концепция "двоичного оператора" легко понятна, например, путем рисования аналогии с простым дополнением или объединением двух множеств - или карт, в этом случае.
Возможная не столь очевидная техническая причина может заключаться в том, что должна быть возможность обеспечить реализацию этого метода, который внутренне не сможет справиться с BiFunction
. Но, учитывая, что BinaryOperator
- это только специализация, трудно представить, как должна выглядеть такая реализация.