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

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

jukzi
источник
Вы можете опубликовать MyInterface?
Морис Перри
это уже внутри класса
Джукзи
Хм, я пытался, <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и код скомпилируется.

michalk
источник
Это интересный тип. Но все же я ищу определение метода «with», который сделает компилятор вызывающим без какого-либо преобразования. В любом случае, спасибо за эту идею.
Джукзи
@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
  • От нулевого типа до нулевого типа

Как вы можете ясно видеть, не существует неявного преобразования бокса из 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
}

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

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

Avi
источник
Спасибо за всю вашу работу, но я не вижу каких-либо улучшений, чтобы избежать ручного набора типов. Мое наивное понимание состоит в том, что компилятор должен иметь возможность выводить (т.е. типизировать) все, что может сделать человек.
Джукзи
@jukzi Мое наивное понимание такое же, но по какой-то причине это не работает. Я придумала обходной путь, который в значительной степени достигает желаемого эффекта,
Avi
Еще раз спасибо. Но ваше новое предложение слишком широкое (см. Stackoverflow.com/questions/58337639 ), поскольку оно позволяет компилировать ".with (MyInterface :: getNumber," Я НЕ НОМЕР ")";
Джукзи
Ваше предложение «Компилятор не может определить тип ссылки на метод и 4L одновременно» - это круто. Но я просто хочу это наоборот. компилятор должен попробовать Number на основе первого параметра и выполнить расширение до Number второго параметра.
Джукзи
1
WHAAAAAAAAAAAT? Почему BiConsumer работает как задумано, а функция - нет? Я не понимаю идею. Я признаю, что это именно та безопасность, которую я хотел, но, к сожалению, не работает с геттерами. ПОЧЕМУ? ПОЧЕМУ?
Джукзи
1

Кажется, что компилятор использовал значение 4L, чтобы решить, что R - Long, а getNumber () возвращает Number, который не обязательно является 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");

    }
}
machfour
источник
"with (MyInterface :: getNumber," NOT NUMBER ")" не должен компилироваться
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.

njzk2
источник
0

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

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

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

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

Hoopje
источник
Ваш код будет компилироваться, with( getNumber,"NO NUMBER")что нежелательно. Также неверно, что дженерики всегда инвариантны (см. Stackoverflow.com/a/58378661/9549750, чтобы доказать, что дженерики сеттеров ведут себя не так, как дженерики )
jukzi
@jukzi Ах, мое решение уже было предложено Avi. Очень плохо... :-). Кстати, это правда , что мы можем присвоить Thing<Cat>к Thing<? extends Animal>переменной, но для реальной ковариации я ожидал бы , что Thing<Cat>может быть назначен Thing<Animal>. Другие языки, такие как Kotlin, позволяют определять ко-и контравариантные переменные типа.
Hoopje