Несоответствие в сигнатурах метода 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 - это только специализация, трудно представить, как должна выглядеть такая реализация.