Це справді цікаве питання. Відповідь, боюся, складна.
тл; д-р
Розробка різниці передбачає досить глибоке зчитування специфікації виводу 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
. Розбивши метод на два підписи, він також краще виражає наміри того, що ви намагаєтесь зробити, з точки зору безпеки типу:
- Перший метод встановлює клас (
With
), який визначає тип на основі посилання на метод.
- Метод scond (
of
) обмежує тип value
сумісного з тим, що ви раніше встановили.
Єдиний спосіб, коли майбутня версія мови зможе це скомпілювати, якщо реалізована повна типізація качок, що здається малоймовірним.
Останнє зауваження, щоб зробити все це нерелевантним: я вважаю, що Mockito (і, зокрема, його функціональність заглушки ), в основному, вже може робити те, що ви намагаєтеся досягти, зі своїм "типовим безпечним загальним конструктором". Можливо, ви могли просто використовувати це замість цього?
Повне (іш) пояснення
Я буду працювати через процедуру виводу типу для і 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
Це зводиться до зв'язаного набору 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
.
Отже, початковий набір обмежень для умовиведення застосовності , C , є:
"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}
.
Використовуючи третій зв'язаний ( String <: R
), ми інстанціюємо R = String
та включаємо це у наш зв'язаний набір. R
зараз вирішено, і другий зв’язок фактично стає F <: Supplier<String>
.
Використовуючи (переглянуту) другу межу, ми створюємо інстанцію F = Supplier<String>
. F
зараз вирішено.
Тепер, коли F
це вирішено, ми можемо продовжувати зменшення , використовуючи нове обмеження:
TypeInference::getLong
сумісний з Supplier<String>
- ... зводиться до
Long
сумісного з String
- ... що зводиться до хибного
... і ми отримуємо помилку компілятора!
Додаткові примітки до "Розширеного прикладу"
Розширений Приклад в питанні виглядає на кілька цікавих випадках, які безпосередньо не охоплені вище виробками:
- Якщо тип значення є підтипом методу return type (
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>
. Це описано у зв'язаній документації щодо зменшення.
Я залишаю читачеві вправу для того, щоб ретельно пропрацювати одну з перерахованих вище процедур, озброївшись цією частиною додаткових знань, щоб продемонструвати собі саме, чому конкретна виклик робить чи ні.