Это действительно интересный вопрос. Боюсь, ответ сложный.
ТЛ; др
Выяснение различий включает в себя довольно глубокое чтение спецификации логического вывода типов Java , но в основном сводится к следующему:
- При прочих равных, компилятор выводит наиболее конкретный тип, который он может.
- Однако, если он может найти на замену для параметра типа , который удовлетворяет все требования, то компиляция будет добиться успеха, однако расплывчатой замена оказывается.
- Ибо
with
есть (по-видимому, расплывчатая) замена, которая удовлетворяет всем требованиям R
:Serializable
- Для
withX
, введение дополнительного параметра типа F
вынуждает компилятор R
сначала разрешать , не принимая во внимание ограничение F extends Function<T,R>
. R
разрешается до (гораздо более конкретно), String
что означает, что вывод F
неудач.
Эта последняя точка пули является самой важной, но также и самой волнистой. Я не могу придумать лучшего и краткого формулировки, поэтому, если вам нужны более подробные сведения, я предлагаю вам прочитать полное объяснение ниже.
Это намеренное поведение?
Я собираюсь выйти на конечности здесь и сказать нет .
Я не предполагаю, что в спецификации есть ошибка, более того, что (в случае withX
) разработчики языка подняли руки и сказали: «В некоторых ситуациях вывод типов становится слишком сложным, поэтому мы просто потерпим неудачу» . Несмотря на то, что поведение компилятора по отношению к withX
тому, что вам нужно, я бы посчитал, что это побочный побочный эффект текущей спецификации, а не положительно задуманное дизайнерское решение.
Это важно, потому что это дает вопрос: следует ли мне полагаться на такое поведение в дизайне моего приложения? Я бы сказал, что вы не должны этого делать, потому что вы не можете гарантировать, что будущие версии языка будут продолжать вести себя таким образом.
Хотя разработчики языка очень стараются не ломать существующие приложения при обновлении своих спецификаций / дизайна / компилятора, проблема заключается в том, что поведение, на которое вы хотите положиться, - это то, где компилятор в настоящее время терпит неудачу (то есть не существующее приложение ). Обновления Langauge постоянно превращают некомпилируемый код в компилируемый. Например, следующий код может быть гарантировано не компилировать в Java 7, но будет компилировать в Java 8:
static Runnable x = () -> System.out.println();
Ваш вариант использования не отличается.
Еще одна причина, по которой я буду осторожен при использовании вашего withX
метода, - это F
сам параметр. Как правило, параметр универсального типа в методе (который не указан в возвращаемом типе) существует для связывания типов нескольких частей подписи вместе. Это говорит:
Мне все равно, что T
есть, но хочу быть уверенным, что везде, где я использую, T
это один и тот же тип.
Таким образом, логично, что мы ожидаем, что каждый параметр типа появится как минимум дважды в сигнатуре метода, в противном случае «он ничего не делает». F
в вашей withX
подписи появляется только один раз, что подсказывает мне использование параметра типа, не соответствующего цели этой функции языка.
Альтернативная реализация
Один из способов реализовать это немного более «намеченным образом» - разделить ваш with
метод на цепочку из 2:
public class Builder<T> {
public final class With<R> {
private final Function<T,R> method;
private With(Function<T,R> method) {
this.method = method;
}
public Builder<T> of(R value) {
// TODO: Body of your old 'with' method goes here
return Builder.this;
}
}
public <R> With<R> with(Function<T,R> method) {
return new With<>(method);
}
}
Это может быть использовано следующим образом:
b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error
Это не включает посторонний параметр типа, как у вас withX
. Разбивая метод на две подписи, он также лучше выражает намерение того, что вы пытаетесь сделать, с точки зрения безопасности типов:
- Первый метод устанавливает class (
With
), который определяет тип на основе ссылки на метод.
- Метод scond (
of
) ограничивает тип value
совместимости с тем, что вы ранее настроили.
Единственный способ, которым будущая версия языка сможет скомпилировать это, - это реализовать полную типизацию утки, что кажется маловероятным.
И последнее замечание, чтобы сделать всю эту вещь неактуальной: я думаю, что Mockito (и, в частности, его функциональность заглушки ), в принципе, может уже делать то, что вы пытаетесь достичь с помощью вашего «универсального конструктора безопасных типов». Может быть, вы могли бы просто использовать это вместо этого?
Полное (ish) объяснение
Я собираюсь проработать процедуру вывода типа для обоих with
и withX
. Это довольно долго, поэтому принимайте это медленно. Несмотря на то, что я долго, я все еще оставил довольно много деталей. Вы можете обратиться к спецификации для получения более подробной информации (перейдите по ссылкам), чтобы убедиться, что я прав (возможно, я допустил ошибку).
Также, чтобы немного упростить ситуацию, я собираюсь использовать более минимальный пример кода. Основное отличие заключается в том , что она меняет местами вне Function
для Supplier
, так что есть меньше типов и параметров в игре. Вот полный фрагмент, который воспроизводит поведение, которое вы описали:
public class TypeInference {
static long getLong() { return 1L; }
static <R> void with(Supplier<R> supplier, R value) {}
static <R, F extends Supplier<R>> void withX(F supplier, R value) {}
public static void main(String[] args) {
with(TypeInference::getLong, "Not a long"); // Compiles
withX(TypeInference::getLong, "Also not a long"); // Does not compile
}
}
Работа Давайте через тип вывод применимости и логический вывод типа процедуру для каждого вызова метода , в своей очереди:
with
У нас есть:
with(TypeInference::getLong, "Not a long");
Начальное связанное множество, B 0 , равно:
Все выражения параметров имеют отношение к применимости .
Следовательно, исходный набор ограничений для вывода применимости , С , является:
TypeInference::getLong
совместим с Supplier<R>
"Not a long"
совместим с R
Это сводит к ограниченному набору B 2 из:
R <: Object
(от B 0 )
Long <: R
(из первого ограничения)
String <: R
(из второго ограничения)
Так как это не содержит связанный « ЛОЖЬ », и (я предполагаю) разрешение на R
преуспевает (предоставление Serializable
), то вызов применим.
Итак, мы переходим к выводу типа вызова .
Новый набор ограничений C со связанными входными и выходными переменными:
TypeInference::getLong
совместим с Supplier<R>
- Входные переменные: нет
- Выходные переменные:
R
Это не содержит взаимозависимостей между входными и выходными переменными, поэтому может быть уменьшено за один шаг, и окончательный набор границ, B 4 , такой же, как B 2 . Следовательно, разрешение преуспевает как прежде, и компилятор вздыхает с облегчением!
withX
У нас есть:
withX(TypeInference::getLong, "Also not a long");
Начальное связанное множество, B 0 , равно:
R <: Object
F <: Supplier<R>
Только выражение второго параметра имеет отношение к применимости . Первый ( TypeInference::getLong
) нет, потому что он соответствует следующему условию:
Если m
это универсальный метод, и вызов метода не предоставляет явных аргументов типа, лямбда-выражения с явным типом или точного ссылочного выражения метода, для которого соответствующий целевой тип (как получено из сигнатуры m
) является параметром типа m
.
Следовательно, исходный набор ограничений для вывода применимости , С , является:
"Also not a long"
совместим с R
Это сводит к ограниченному набору B 2 из:
R <: Object
(от B 0 )
F <: Supplier<R>
(от B 0 )
String <: R
(из ограничения)
Опять же , так как это не содержит связанную « ЛОЖЬ », и разрешение на R
преуспевает (предоставление String
), то вызов применим.
Вывод типа вызова еще раз ...
На этот раз новый набор ограничений C с соответствующими входными и выходными переменными:
TypeInference::getLong
совместим с F
- Входные переменные:
F
- Выходные переменные: нет
Опять же, у нас нет взаимозависимостей между входными и выходными переменными. Однако на этот раз, то есть входной переменный ( F
), поэтому мы должны решить это , прежде чем снижение . Итак, начнем с нашего связанного множества B 2 .
Мы определяем подмножество V
следующим образом:
Учитывая набор переменных логического вывода для разрешения, позвольте V
быть объединением этого набора и всех переменных, от которых зависит разрешение по крайней мере одной переменной в этом наборе.
По второй оценке в B 2 разрешение F
зависит от R
, т V := {F, R}
.
Мы выбираем подмножество в V
соответствии с правилом:
позвольте { α1, ..., αn }
быть непустым подмножеством необоснованных переменных в V
такой, что i) для всех i (1 ≤ i ≤ n)
, если αi
зависит от разрешения переменной β
, то либо β
имеет экземпляр, либо есть некоторые j
такие, которые β = αj
; и ii) не существует непустого собственного подмножества { α1, ..., αn }
с этим свойством.
Единственное подмножество, V
которое удовлетворяет этому свойству, это {R}
.
Используя третий bound ( String <: R
), мы создаем его R = String
и включаем в наш ограниченный набор. R
теперь решен, и вторая граница эффективно становится F <: Supplier<String>
.
Используя (пересмотренную) вторую оценку, мы создаем экземпляр F = Supplier<String>
. F
сейчас решено.
Теперь, когда F
это решено, мы можем приступить к сокращению , используя новое ограничение:
TypeInference::getLong
совместим с Supplier<String>
- ... сводится к
Long
совместим с String
- ... который сводится к ложному
... и мы получаем ошибку компилятора!
Дополнительные примечания к «Расширенному примеру»
Расширенный Пример в вопросе выглядит на несколько интересных случаях, которые непосредственно не охвачены выше выработками:
- Где тип значения является подтипом метода, возвращаемого типом (
Integer <: Number
)
- Где функциональный интерфейс является контравариантным в предполагаемом типе (то есть,
Consumer
а не Supplier
)
В частности, 3 из данных вызовов выделяются как потенциально предполагающие «отличное» поведение компилятора от описанного в пояснениях:
t.lettBe(t::setNumber, "NaN"); // Does not compile :-)
t.letBeX(t::getNumber, 2); // !!! Does not compile :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)
Второй из этих 3 будет проходить точно такой же процесс вывода, как withX
указано выше (просто заменить Long
на Number
и String
с Integer
). Это иллюстрирует еще одну причину , почему вы не должны полагаться на это не смогло поведения типа логического вывода для вашего дизайна класса, как неспособность собрать здесь, скорее всего , не желательное поведение.
Для других 2 (и, действительно, для любых других вызовов, связанных с тем, через что Consumer
вы хотите работать), поведение должно быть очевидным, если вы работаете с процедурой вывода типа, изложенной для одного из методов выше (то есть with
для первого, withX
для в третьих). Есть только одно маленькое изменение, на которое нужно обратить внимание:
- Ограничение по первому параметру (
t::setNumber
совместимо с Consumer<R>
) будет уменьшено до R <: Number
того, Number <: R
что было сделано для Supplier<R>
. Это описано в связанной документации по сокращению.
Я оставляю читателю в качестве упражнения упражнение для выполнения одной из вышеуказанных процедур, вооруженных этим дополнительным знанием, чтобы продемонстрировать себе, почему конкретный вызов компилируется или не компилируется.