Почему ява не может определить супертип? - PullRequest
19 голосов
/ 14 октября 2019

Все мы знаем, что Лонг расширяется 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 равен Number, это несовместимо с типом возвращаемого значения дескриптора: Long

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

Ответы [ 6 ]

10 голосов
/ 14 октября 2019

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

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

4 голосов
/ 14 октября 2019

Ключ к вашей ошибке находится в общем объявлении типа 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
  • Из байта типа в тип Byte
  • От типа short к типу Short
  • От типа char к типу Character
  • От типа int к типу Integer
  • От типа long к типу Long
  • Сот типа float до типа Float
  • От типа double до типа Double
  • От типа null до типа null

Как вы можете видеть,не является неявным преобразованием бокса из long в Number, и расширение преобразования из Long в Number может происходить, только когда компилятор уверен, что ему требуется число, а не LongПоскольку существует конфликт между ссылкой на метод, которая требует Number, и 4L, который предоставляет Long, компилятор (по какой-то причине ???) не может сделать логический скачок, что Long is-a Number, и вывести, что F isa 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>), так что вам даже не придется беспокоиться о результирующем объектебыть измененным. Честно говоря, это просто позор, что требуется так много усилий, чтобы получить безопасный для типов компоновщик с гибкими полями, но это, вероятно, невозможно без некоторых новых функций, генерации кода или раздражающего количества размышлений.

1 голос
/ 14 октября 2019

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

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

0 голосов
/ 15 октября 2019

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

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

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

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

0 голосов
/ 15 октября 2019

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

// 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.

0 голосов
/ 15 октября 2019

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

    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...