Це справді цікаве питання. Відповідь, боюся, складна.
тл; д-р
Розробка різниці передбачає досить глибоке зчитування специфікації виводу 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>. Це описано у зв'язаній документації щодо зменшення.
Я залишаю читачеві вправу для того, щоб ретельно пропрацювати одну з перерахованих вище процедур, озброївшись цією частиною додаткових знань, щоб продемонструвати собі саме, чому конкретна виклик робить чи ні.