Это действительно интересный вопрос. Боюсь, что ответ сложный.
tl; dr
Чтобы выявить разницу, нужно немного углубиться в чтение спецификации вывода типа Java , нов основном сводится к следующему:
- При прочих равных, компилятор выводит наиболее специфичный тип, который он может.
- Однако, если он может найти a замена для параметра типа, который удовлетворяет всем требованиям, тогда компиляция будет успешной, однако расплывчато замена оказывается.
- Для
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
появляется только один раз в подписи, что предлагает мне использовать параметр типа, не совпадающий с намерением этой функции языка.
Anальтернативная реализация
Один из способов реализовать это немного более "намеченным образом" - разделить ваш метод 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
. Разбивая метод на две подписи, он также лучше выражает намерение того, что вы пытаетесь сделать, с точки зрения безопасности типов:
- Первый метод устанавливает класс (
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 , это:
Все выражения параметров относятся к применимости .
Следовательно, начальное ограничение, установленное для логического вывода , C , равно:
TypeInference::getLong
совместим с Supplier<R>
"Not a long"
совместим с R
This уменьшает до связанного множества B 2 из:
R <: Object
(от B 0 ) Long <: R
(из первого ограничения) String <: R
(из второго ограничения)
Поскольку это не содержит границу ' false ' и (я предполагаю) разрешение из R
успешно (дает Serializable
), тогдавызов применим.
Итак, мы переходим к выводу типа вызова .
Новый набор ограничений C , с соответствующими входными и выходными переменными, это:
TypeInference::getLong
совместим с Supplier<R>
- Входные переменные: нет
- Выходные переменные:
R
Не содержит взаимозависимостей между input и вывод переменных, поэтому можно уменьшить за один шаг, а окончательный набор границ B 4 , совпадает с B 2 . Следовательно, разрешение успешно, как и прежде, и компилятор вздыхает с облегчением!
withX
У нас есть:
withX(TypeInference::getLong, "Also not a long");
Начальный ограниченный набор, B 0 , составляет:
R <: Object
F <: Supplier<R>
Только второе выражение параметра имеет значение , относящееся к применимости . Первый (TypeInference::getLong
) - нет, потому что он удовлетворяет следующему условию:
Если m
является универсальным методом и вызов метода не предоставляет явных аргументов типа, лямбда с явно заданным типомвыражение или точное ссылочное выражение метода, для которого соответствующий целевой тип (полученный из сигнатуры m
) является параметром типа m
.
Следовательно, начальное ограничение установлено для вывод применимости , C , это:
"Also not a long"
совместим с R
Это уменьшает до ограниченного набора B 2 из:
R <: Object
(от B 0 ) F <: Supplier<R>
(из B 0 ) String <: R
(из ограничения)
Опять же, поскольку это не содержит границы ' false ' и разрешение из R
успешно (дает String
), тогда вызов применим.
Вывод типа вызова еще раз ...
На этот раз,новый набор ограничений C со связанными входными и выходными переменными:
TypeInference::getLong
- этосовместим с F
- Входные переменные:
F
- Выходные переменные: нет
Опять же, у нас нет взаимозависимостей между входными и выходными переменными. Однако на этот раз является * входной переменной (F
), поэтому мы должны разрешить , прежде чем пытаться уменьшить . Итак, мы начинаем с нашего ограниченного набора B 2 .
Мы определяем подмножество V
следующим образом:
Учитывая набор переменных логического вывода для разрешения, пусть V
будет объединением этого набора и всех переменных, от которых зависит разрешение хотя бы одной переменной в этом наборе.
Byвторая граница в B 2 , разрешение F
зависит от R
, поэтому V := {F, R}
.
Выбираемподмножество V
в соответствии с правилом:
пусть { α1, ..., αn }
будет непустым подмножеством необоснованных переменных в V
, таких, что i) для всех i (1 ≤ i ≤ n)
, если αi
зависит от разрешения переменной β
, тогда либо β
имеет экземпляр, либо есть какой-то j
такой, что β = αj
;и ii) не существует непустого собственного подмножества { α1, ..., αn }
с этим свойством.
Единственное подмножество V
, удовлетворяющее этому свойству, - {R}
.
Используя третью границу (String <: R
), мы создаем R = String
и включаем это в наш набор ограничений. R
теперь разрешено, и вторая граница фактически становится F <: Supplier<String>
.
Используя (пересмотренную) вторую границу, мы создаем F = Supplier<String>
. F
теперь разрешено.
Теперь, когда разрешено F
, мы можем приступить к сокращению , используя новое ограничение:
TypeInference::getLong
совместим с Supplier<String>
- ... уменьшается до
Long
совместим с String
- ... который уменьшается до false
... и мы получаем ошибку компилятора!
Дополнительные примечания к «Расширенному примеру»
В расширенном примере в вопросе рассматриваются несколько интересных случаев, которые непосредственно не охватываются приведенными выше работами:
- Где тип значения - подтип типа возврата метода (
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>
. Это описано в сопроводительной документации по сокращению.
Я оставляю читателю в качестве упражнения осторожную проработку одной из вышеуказанных процедур, вооруженных этим дополнительным знанием, чтобы продемонстрировать себяпочему конкретный вызов компилируется или не компилируется.