Чому параметр типу сильніше, ніж параметр методу


12

Чому це

public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}

тоді суворіші

public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}

Це подальше дослідження на тему Чому тип повернення лямбда не перевіряється під час компіляції . Я виявив , використовуючи метод , withX()як

.withX(MyInterface::getLength, "I am not a Long")

видає потрібну помилку часу компіляції:

Тип getLength () від типу BuilderExample.MyInterface довгий, це несумісне з типом повернення дескриптора: String

при використанні методу with()цього немає.

повний приклад:

import java.util.function.Function;

public class SO58376589 {
  public static class Builder<T> {
    public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {
      return this;
    }

    public <R> Builder<T> with(Function<T, R> getter, R returnValue) {
      return this;
    }

  }

  static interface MyInterface {
    public Long getLength();
  }

  public static void main(String[] args) {
    Builder<MyInterface> b = new Builder<MyInterface>();
    Function<MyInterface, Long> getter = MyInterface::getLength;
    b.with(getter, 2L);
    b.with(MyInterface::getLength, 2L);
    b.withX(getter, 2L);
    b.withX(MyInterface::getLength, 2L);
    b.with(getter, "No NUMBER"); // error
    b.with(MyInterface::getLength, "No NUMBER"); // NO ERROR !!
    b.withX(getter, "No NUMBER"); // error
    b.withX(MyInterface::getLength, "No NUMBER"); // error !!!
  }
}

javac SO58376589.java

SO58376589.java:32: error: method with in class Builder<T> cannot be applied to given types;
    b.with(getter, "No NUMBER"); // error
     ^
  required: Function<MyInterface,R>,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where R,T are type-variables:
    R extends Object declared in method <R>with(Function<T,R>,R)
    T extends Object declared in class Builder
SO58376589.java:34: error: method withX in class Builder<T> cannot be applied to given types;
    b.withX(getter, "No NUMBER"); // error
     ^
  required: F,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where F,R,T are type-variables:
    F extends Function<MyInterface,R> declared in method <R,F>withX(F,R)
    R extends Object declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
SO58376589.java:35: error: incompatible types: cannot infer type-variable(s) R,F
    b.withX(MyInterface::getLength, "No NUMBER"); // error
           ^
    (argument mismatch; bad return type in method reference
      Long cannot be converted to String)
  where R,F,T are type-variables:
    R extends Object declared in method <R,F>withX(F,R)
    F extends Function<T,R> declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
3 errors

Розширений приклад

Наступний приклад показує різну поведінку методу та параметру типу, зведених до Постачальника. Крім того, він показує різницю в поведінці споживача для параметра типу. І це показує, що це не має значення, якщо це параметр Споживача чи Постачальника.

import java.util.function.Consumer;
import java.util.function.Supplier;
interface TypeInference {

  Number getNumber();

  void setNumber(Number n);

  @FunctionalInterface
  interface Method<R> {
    TypeInference be(R r);
  }

  //Supplier:
  <R> R letBe(Supplier<R> supplier, R value);
  <R, F extends Supplier<R>> R letBeX(F supplier, R value);
  <R> Method<R> let(Supplier<R> supplier);  // return (x) -> this;

  //Consumer:
  <R> R lettBe(Consumer<R> supplier, R value);
  <R, F extends Consumer<R>> R lettBeX(F supplier, R value);
  <R> Method<R> lett(Consumer<R> consumer);


  public static void main(TypeInference t) {
    t.letBe(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBe(t::setNumber, (Number) 2); // Compiles :-)
    t.letBe(t::getNumber, 2); // Compiles :-)
    t.lettBe(t::setNumber, 2); // Compiles :-)
    t.letBe(t::getNumber, "NaN"); // !!!! Compiles :-(
    t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

    t.letBeX(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBeX(t::setNumber, (Number) 2); // Compiles :-)
    t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
    t.lettBeX(t::setNumber, 2); // Compiles :-)
    t.letBeX(t::getNumber, "NaN"); // Does not compile :-)
    t.lettBeX(t::setNumber, "NaN"); // Does not compile :-)

    t.let(t::getNumber).be(2); // Compiles :-)
    t.lett(t::setNumber).be(2); // Compiles :-)
    t.let(t::getNumber).be("NaN"); // Does not compile :-)
    t.lett(t::setNumber).be("NaN"); // Does not compile :-)
  }
}

1
Через умовивід останнього. Хоча обидва вони базуються на випадку використання, який потрібно реалізувати. Для вашого колишнього може бути суворим і добрим. Для гнучкості хтось інший може віддавати перевагу останньому.
Наман

Ви намагаєтеся скласти це в Eclipse? Пошук рядків помилок формату, який ви вставили, дозволяє припустити, що це специфічна помилка Eclipse (ecj). Чи виникає у вас така ж проблема при компілюванні із сировиною javacчи інструментом побудови, як Gradle чи Maven?
користувач31601

@ user31601 Я додав повний приклад з виведенням javac. Повідомлення про помилки мають дещо інший формат, але все ж затемнення та javac мають однакову поведінку
jukzi

Відповіді:


12

Це справді цікаве питання. Відповідь, боюся, складна.

тл; д-р

Розробка різниці передбачає досить глибоке зчитування специфікації виводу 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 , становить:

  • R <: Object

Всі вирази параметрів стосуються застосовності .

Отже, початковий набір обмежень для умовиведення застосовності , 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 .

  1. Підмножину визначаємо Vтак:

    Враховуючи набір змінних висновків для вирішення, нехай Vбуде об'єднання цього набору та всіх змінних, від яких залежить роздільна здатність принаймні однієї змінної у цьому наборі.

    До другої межі в B 2 роздільна здатність Fзалежить від R, так V := {F, R}.

  2. Вибираємо підмножину Vвідповідно до правила:

    нехай { α1, ..., αn }буде не порожнім підмножиною ненавмисних змінних Vтаким чином, що i) для всіх i (1 ≤ i ≤ n), якщо це αiзалежить від роздільної здатності змінної β, то або βє екземпляр, або є jтакий, що β = αj; і ii) не існує не порожнього належного набору { α1, ..., αn }з цим властивістю.

    Єдиний підмножина, Vщо задовольняє цій властивості, - це {R}.

  3. Використовуючи третій зв'язаний ( String <: R), ми інстанціюємо R = Stringта включаємо це у наш зв'язаний набір. Rзараз вирішено, і другий зв’язок фактично стає F <: Supplier<String>.

  4. Використовуючи (переглянуту) другу межу, ми створюємо інстанцію F = Supplier<String>. Fзараз вирішено.

Тепер, коли Fце вирішено, ми можемо продовжувати зменшення , використовуючи нове обмеження:

  1. TypeInference::getLong сумісний з Supplier<String>
  2. ... зводиться до Long сумісного з String
  3. ... що зводиться до хибного

... і ми отримуємо помилку компілятора!


Додаткові примітки до "Розширеного прикладу"

Розширений Приклад в питанні виглядає на кілька цікавих випадках, які безпосередньо не охоплені вище виробками:

  • Якщо тип значення є підтипом методу 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>. Це описано у зв'язаній документації щодо зменшення.

Я залишаю читачеві вправу для того, щоб ретельно пропрацювати одну з перерахованих вище процедур, озброївшись цією частиною додаткових знань, щоб продемонструвати собі саме, чому конкретна виклик робить чи ні.


Дуже поглиблене, добре вивчене та сформульоване. Дякую!
Забузар

@ user31601 Ви можете, будь ласка, вказати, де відіграється різниця постачальника для споживача. Для цього я додав розширений приклад до початкового запитання. Він показує коваріантну, противаріантну та інваріантну поведінку для різних версій letBe (), letBeX () та let (). Be () залежно від постачальника / споживача.
jukzi

@jukzi Я додав кілька додаткових приміток, але у вас має бути достатньо інформації, щоб самостійно опрацювати ці нові приклади.
користувач31601

Це цікаво: так багато особливих випадків у 18.2.1. для лямбдів та методичних посилань, де я б не очікував, що я буду мати особливий випадок для них з мого наївного розуміння. І, мабуть, жоден звичайний розробник не очікував.
jukzi

Ну, мабуть, причина полягає в тому, що за допомогою лямбда та посилань на метод компілятору потрібно вирішити, який саме тип лямбда повинен реалізувати - він повинен зробити вибір! Наприклад, TypeInference::getLongможе бути придурком Supplier<Long>або Supplier<Serializable>і Supplier<Number>т. Д. , Але головне, що він може реалізувати лише один із них (як і будь-який інший клас)! Це відрізняється від усіх інших виразів, де реалізовані типи всі відомі наперед, і компілятор просто повинен з’ясувати, чи відповідає один з них обмеженням.
користувач31601
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.