Чому Java не може зробити висновок про супертип?


19

Ми всі знаємо, що Лонг розширюється Number. То чому це не складається?

І як визначити метод withтак, щоб програма компілювалась без будь-якого вручну?

import java.util.function.Function;

public class Builder<T> {
  static public interface MyInterface {
    Number getNumber();
    Long getLong();
  }

  public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue) {
    return null;//TODO
  }

  public static void main(String[] args) {
    // works:
    new Builder<MyInterface>().with(MyInterface::getLong, 4L);
    // works:
    new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
    // works:
    new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
    // works:
    new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
    // compilation error: Cannot infer ...
    new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
    // compilation error: Cannot infer ...
    new Builder<MyInterface>().with(MyInterface::getNumber, Long.valueOf(4));
    // compiles but also involves typecast (and Casting Number to Long is not even safe):
    new Builder<MyInterface>().with( myInterface->(Long) myInterface.getNumber(), 4L);
    // compiles but also involves manual conversion:
    new Builder<MyInterface>().with(myInterface -> myInterface.getNumber().longValue(), 4L);
    // compiles (compiler you are kidding me?): 
    new Builder<MyInterface>().with(castToFunction(MyInterface::getNumber), 4L);

  }
  static <X, Y> Function<X, Y> castToFunction(Function<X, Y> f) {
    return f;
  }

}
  • Неможливо зробити аргументи типів для <F, R> with(F, R)
  • Тип getNumber () від типу Builder.MyInterface - число, це несумісне з типом повернення дескриптора: Long

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


Чи можете ви дописувати MyInterface?
Моріс Перрі

його вже всередині класу
jukzi

Хм, я спробував, <F extends Function<T, R>, R, S extends R> Builder<T> with(F getter, S returnValue)але отримав java.lang.Number cannot be converted to java.lang.Long), що дивно, тому що я не бачу, де компілятор отримує думку про те, що потрібно повернути значення, getterяке потрібно повернути returnValue.
jingx

@jukzi ОК. Вибачте, я пропустив це.
Моріс Перрі

Зміна Number getNumber()в <A extends Number> A getNumber()марку речі роботи. Не маю уявлення, чи це ви хотіли. Як говорили інші, проблема полягає в тому, що це MyInterface::getNumberможе бути функція, яка повертається, Doubleнаприклад, а не Long. Ваша декларація не дозволяє компілятору звузити тип повернення на основі іншої присутньої інформації. Використовуючи загальний тип повернення, ви дозволяєте компілятору це робити, отже, він працює.
Джакомо Альзетта

Відповіді:


9

Цей вираз:

new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

можна переписати як:

new Builder<MyInterface>().with(myInterface -> myInterface.getNumber(), 4L);

Враховуючи підпис методу:

public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue)
  • R буде зроблено висновок про Long
  • F буде Function<MyInterface, Long>

і ви Function<MyInterface, Number>передаєте посилання на метод, який буде заражений, оскільки це ключ - як компілятор повинен передбачити, що ви насправді хочете повернутися Longз функції з такою підписом? Це не спричинить занепаду для вас.

Оскільки Numberє надкласовим Longі Numberне обов'язково є Long(тому він не компілюється) - вам доведеться чітко подавати заявку самостійно:

new Builder<MyInterface>().with(myInterface -> (Long) myInterface.getNumber(), 4L);

що робить , Fщоб бути Function<MyIinterface, Long>або передавати загальні аргументи в явному вигляді при виклику методу , як ви зробили:

new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);

і знати, що Rбуде розглянуто, як Numberі код складеться.


Це цікавий набір. Але все ж я шукаю визначення методу "з", який зробить абонент компілювати без будь-якого виступу. У будь-якому випадку дякую за цю ідею.
jukzi

@jukzi Ви не можете. Не має значення, як withнаписано ваше . У вас є MJyInterface::getNumberтип Function<MyInterface, Number>так, R=Numberі тоді ви також маєте R=Longінший аргумент (пам’ятайте, що літерали Java не є поліморфними!). У цей момент компілятор зупиняється, тому що не завжди можливо перетворити a Numberв a Long. Єдиний спосіб виправити це - змінити MyInterfaceна використання <A extends Number> Numberяк тип повернення, це змушує компілятор мати R=Aі тоді, R=Longі оскільки A extends Numberвін може замінитиA=Long
Giacomo Alzetta

4

Ключ до вашої помилку в загальній декларації типу F: F extends Function<T, R>. Заява, яка не працює: new Builder<MyInterface>().with(MyInterface::getNumber, 4L);По-перше, у вас є нова Builder<MyInterface>. Тому декларація класу передбачає T = MyInterface. Відповідно до вашої декларації with, Fповинно бути таке Function<T, R>, що є Function<MyInterface, R>в цій ситуації. Тому параметр getterповинен приймати MyInterfaceяк параметр (задоволений посиланнями методу MyInterface::getNumberі MyInterface::getLong), і повертати R, який повинен бути того ж типу, що і другий параметр функції with. Тепер давайте подивимось, чи це стосується всіх ваших випадків:

// T = MyInterface, F = Function<MyInterface, Long>, R = Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L explicitly widened to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().<Function<MyInterface, Number>, Number>with(MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Long
// F = Function<T, not R> violates definition, therefore compilation error occurs
// Compiler cannot infer type of method reference and 4L at the same time, 
// so it keeps the type of 4L as Long and attempts to infer a match for MyInterface::getNumber,
// only to find that the types don't match up
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

Ви можете "виправити" цю проблему за допомогою наступних варіантів:

// stick to Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// stick to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// explicitly convert the result of getNumber:
new Builder<MyInterface>().with(myInstance -> (Long) myInstance.getNumber(), 4L);
// explicitly convert the result of getLong:
new Builder<MyInterface>().with(myInterface -> (Number) myInterface.getLong(), (Number) 4L);

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

Причина того, що ви не можете зробити це без кастингу, полягає в наступному, із специфікації мови Java :

Перетворення боксу трактує вирази примітивного типу як вирази відповідного типу опор. Зокрема, наступні дев'ять конверсій називаються перетвореннями боксу :

  • Від типу булевого до типу булевого
  • Від типу байт до типу Байт
  • Від типу короткий до типу Короткий
  • Від типу char до типу Character
  • Від типу int до типу Integer
  • Від типу long до типу Long
  • Від типу float до типу Float
  • Від типу double до типу Double
  • Від нульового типу до нульового типу

Як ви добре бачите, немає неявного перетворення боксу з довгого в число, а розширення перетворення з довгого в число може відбуватися лише тоді, коли компілятор впевнений, що йому потрібне число, а не довге. Оскільки існує конфлікт між посиланням на метод, який вимагає Числа, та 4L, що забезпечує Long, компілятор (чомусь ???) не в змозі зробити логічний стрибок, що Long is-a Number, і вивести, що Fє a Function<MyInterface, Number>.

Натомість мені вдалося вирішити проблему, трохи відредагувавши функцію підпису:

public <R> Builder<T> with(Function<T, ? super R> getter, R returnValue) {
  return null;//TODO
}

Після цієї зміни відбувається наступне:

// doesn't work, as it should not work
new Builder<MyInterface>().with(MyInterface::getLong, (Number), 4L);
// works, as it always did
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// works, as it should work
new Builder<MyInterface>().with(MyInterface::getNumber, (Number)4L);
// works, as you wanted
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

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

public class Builder<T> {

  static public interface MyInterface {
    //setters
    void number(Number number);
    void Long(Long Long);
    void string(String string);

    //getters
    Number number();
    Long Long();
    String string();
  }
  // whatever object we're building, let's say it's just a MyInterface for now...
  private T buildee = (T) new MyInterface() {
    private String string;
    private Long Long;
    private Number number;
    public void number(Number number)
    {
      this.number = number;
    }
    public void Long(Long Long)
    {
      this.Long = Long;
    }
    public void string(String string)
    {
      this.string = string;
    }
    public Number number()
    {
      return this.number;
    }
    public Long Long()
    {
      return this.Long;
    }
    public String string()
    {
      return this.string;
    }
  };

  public <R> Builder<T> with(BiConsumer<T, R> setter, R val)
  {
    setter.accept(this.buildee, val); // take the buildee, and set the appropriate value
    return this;
  }

  public static void main(String[] args) {
    // works:
    new Builder<MyInterface>().with(MyInterface::Long, 4L);
    // works:
    new Builder<MyInterface>().with(MyInterface::number, (Number) 4L);
    // compile time error, as it shouldn't work
    new Builder<MyInterface>().with(MyInterface::Long, (Number) 4L);
    // works, as it always did
    new Builder<MyInterface>().with(MyInterface::Long, 4L);
    // works, as it should
    new Builder<MyInterface>().with(MyInterface::number, (Number)4L);
    // works, as you wanted
    new Builder<MyInterface>().with(MyInterface::number, 4L);
    // compile time error, as you wanted
    new Builder<MyInterface>().with(MyInterface::number, "blah");
  }
}

Надаючи безпечну для типу можливість побудувати об’єкт, сподіваємось, в якийсь момент у майбутньому ми зможемо повернути незмінний об’єкт даних від конструктора (можливо, додавши toRecord()метод в інтерфейс і вказавши конструктора як Builder<IntermediaryInterfaceType, RecordType>), тож вам навіть не потрібно турбуватися про зміну отриманого об’єкта. Чесно кажучи, це абсолютно соромно, що для отримання безпечного для польових будівельників потрібних стільки зусиль, але це, мабуть, неможливо без деяких нових функцій, генерації коду чи прикрої кількості роздумів.


Дякую за всю вашу роботу, але я не бачу жодних вдосконалень у напрямку уникнення ручного набору. Моє наївне розуміння полягає в тому, що компілятор повинен мати можливість робити (тобто набирати текст) все, що може людина.
jukzi

@jukzi Моє наївне розуміння те саме, але з будь-якої причини це не працює так. Я придумав рішення, яке в значній
Avi

Знову дякую. Але ваша нова пропозиція занадто широка (див. Stackoverflow.com/questions/58337639 ), оскільки вона дозволяє компілювати ".with (MyInterface :: getNumber," Я НЕ ЧИСЛО ")";
jukzi

ваше речення "Компілятор не може зробити висновок про тип методу та 4L одночасно", це здорово. Але я просто хочу, щоб це було навпаки. компілятор повинен спробувати Number на основі першого параметра і зробити розширення на Number другого параметра.
джукзі

1
WHAAAAAAAAAAAT? Чому BiConsumer працює за призначенням, поки функція не працює? Я не розумію. Я визнаю, це саме та безпека, яку я хотів, але, на жаль, не працює з дітьми. ЧОМУ ЧОМУ ЧОМУ?
jukzi

1

Здається, компілятор використав значення 4L, щоб вирішити, що R - Long, а getNumber () повертає число, яке не обов'язково є Long.

Але я не впевнений, чому значення має перевагу над методом ...


0

Компілятор Java, як правило, не підходить для виведення декількох / вкладених загальних типів або символів. Часто я не можу щось компілювати без використання допоміжної функції для зйомки або висновку деяких типів.

Але, чи вам справді потрібно зафіксувати точний тип Functionяк F? Якщо ні, можливо, наступні роботи, і як ви бачите, також, здається, працюють з підтипами Function.

import java.util.function.Function;
import java.util.function.UnaryOperator;

public class Builder<T> {
    public interface MyInterface {
        Number getNumber();
        Long getLong();
    }

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

    // example subclass of Function
    private static UnaryOperator<String> stringFunc = (s) -> (s + ".");

    public static void main(String[] args) {
        // works
        new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
        // works
        new Builder<String>().with(stringFunc, "s");

    }
}

"з (MyInterface :: getNumber," НЕ ЧИСЛО ")" не слід компілювати
jukzi

0

Найцікавіша частина полягає в різниці між цими двома рядками, я думаю:

// works:
new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
// compilation error: Cannot infer ...
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

У першому випадку, Tце явно Number, так само, 4Lце також Numberнемає проблем. У другому випадку 4Lє a Long, так Tце a Long, тому ваша функція не сумісна, і Java не може знати, чи ви це мали на увазі Numberчи Long.


0

З таким підписом:

public <R> Test<T> with(Function<T, ? super R> getter, R returnValue)

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

Причина того, що ваша версія не працює, полягає в тому, що посилання на методи Java не мають одного конкретного типу. Натомість вони мають тип, необхідний у заданому контексті. У вашому випадку Rвважається , що це відбувається Longчерез 4L, але getter не може мати тип, Function<MyInterface,Long>оскільки в Java загальні типи інваріантні у своїх аргументах.


Ваш код буде компілювати, with( getNumber,"NO NUMBER")що не бажано. Також не вірно, що дженерики завжди інваріантні (див. Stackoverflow.com/a/58378661/9549750, щоб довести, що дженерики сетерів поводяться інакше, ніж ті, хто
отримує

@jukzi Ах, моє рішення вже було запропоновано Аві. Шкода ... :-). До речі, це правда, що ми можемо призначити змінну Thing<Cat>a Thing<? extends Animal>, але для реальної коваріації я би сподівався, що a Thing<Cat>можна призначити a Thing<Animal>. Інші мови, такі як Котлін, дозволяють визначати змінні типу ко- та противаріантного типу.
Hoopje
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.