Почему ява не может вывести супертип

Мы все знаем, Лонг продлевает номер. Так почему это не компилируется. И как определить метод "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 - Number, это несовместимо с возвращаемым типом дескриптора: Long

Пример использования: Почему лямбда-тип возврата не проверяется во время компиляции

Ответы

Ответ 1

Это выражение:

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, и код скомпилируется.

Ответ 2

Ключ к вашей ошибке находится в общем объявлении типа 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:

Конверсионное преобразование обрабатывает выражения примитивного типа как выражения соответствующего ссылочного типа. В частности, следующие девять преобразований называются преобразованиями в бокс:

  • От типа boolean к типу Boolean
  • От типа байта к типу байта
  • От типа короткий до типа короткий
  • От типа char до типа Character
  • От типа int до типа Integer
  • От типа long к типу Long
  • От типа float к типу Float
  • От типа double до типа Double
  • От нулевого типа до нулевого типа

Как вы можете ясно видеть, не существует неявного преобразования бокса из long в Number, и расширяющееся преобразование из Long в Number может происходить, только когда компилятор уверен, что для него требуется Number, а не Long. Поскольку существует конфликт между ссылкой на метод, которая требует Number, и 4L, который предоставляет Long, компилятор (по какой-то причине???) не может сделать логический скачок, что Long is-a Number, и вывести, что F Function<MyInterface, Number>.

Вместо этого мне удалось решить проблему, слегка отредактировав сигнатуру функции:

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

После этого изменения происходит следующее:

// does not 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);

Edit:
Потратив немного больше времени на это, досадно трудно обеспечить безопасность типов на основе геттеров. Вот рабочий пример, который использует методы установки для обеспечения безопасности типов для строителя:

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 say it 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>), так что вам даже не придется беспокоиться об изменении результирующего объекта. Честно говоря, это просто позор, что требуется так много усилий, чтобы получить безопасный для типов компоновщик с гибкими полями, но это, вероятно, невозможно без некоторых новых функций, генерации кода или раздражающего количества размышлений.

Ответ 3

Похоже, что компилятор использовал значение 4L, чтобы решить, что R - Long, а getNumber() возвращает Number, который не обязательно является Long.

Но я не уверен, почему значение имеет приоритет над методом...

Ответ 4

Строка:

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

эквивалентно:

new Builder<MyInterface>().with(new Function<MyInterface, Long>() {
    @Override
    public Long apply(MyInterface myInterface) {
        return myInterface.getNumber();
    }
}, 4L);

Где getNumber() возвращает Number, который нельзя преобразовать в Long.

В этом случае предоставьте типы явно, как вы уже сделали:

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

Или используйте вместо этого следующее лямбда-выражение:

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

Ответ 5

Компилятор 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");

    }
}

Ответ 6

Я думаю, что самая интересная часть заключается в разнице между этими двумя строками:

// 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 - это Long, поэтому T - это Long, поэтому ваша функция несовместима, и Java не может знать, имели ли вы в виду Number или Long.

Ответ 7

Со следующей подписью:

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

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

Причина, по которой ваша версия не работает, заключается в том, что ссылки на методы Java не имеют одного определенного типа. Вместо этого они имеют тип, который требуется в данном контексте. В вашем случае R определяется как Long из-за 4L, но метод получения не может иметь тип Function<MyInterface,Long>, потому что в Java универсальные типы являются инвариантными в своих аргументах.