Я столкнулся с проблемой со ссылкой на метод и выводом типа.
Родительский класс, который возвращает тип T:
public class Parent {
public <T extends Parent> T get() {
...
}
}
Следующий код не работает:
List<Parent> parentList = new ArrayList<>();
List<Parent> collect = parentList.stream().map(Parent::get).collect(Collectors.toList()); // error
сообщение об ошибке
не существует экземпляра(ов) переменной(ей) типа, поэтому объект соответствует родительской переменной вывода T имеет несовместимые границы: ограничения равенства: родительские нижние границы: объект
Однако следующий успех кода:
List<Parent> parentList = new ArrayList<>();
Function<Parent, Parent> func = Parent::get;
List<Parent> collect = parentList.stream().map(func).collect(Collectors.toList());
Почему компилятор не может вывести возвращаемое значение метода get в классе Parent, хотя это, по крайней мере, тип Parent?
может быть public Parent get() { ... }
, или вам действительно нужен метод, возвращающий другой тип?
Обратите внимание, что подпись этого метода немного странная, поскольку подразумевает, что реализация может угадать тип возвращаемого значения, который ожидает вызывающая сторона (или вызывающая сторона каким-то образом знает, что получить во время компиляции). Единственный способ реализовать такой метод — это непроверяемое приведение типов. Вы уверены, что это настоящая подпись? Я бы посчитал это плохой практикой, и в прошлом я видел худшие проблемы с подобными методами.
Вывод обобщений не охватывает бесконечное пространство вызовов связанных методов.
Компилятор выполняет как внутренний, так и внешний вывод типа.
Наизнанку — это стандартный подход, используемый компилятором. Наизнанку — это вывод, скажем:
List<String> x = someCollection.stream().map(String::toLowerCase).toList();
Сначала определив тип someCollection
, затем выясните, что такое тип that.stream()
, затем какой тип that.map(String::toLowerCase)
и так далее.
Однако его также необходимо применять и снаружи внутрь. Это приводит к появлению лямбда-выражений и ссылок на методы. В Java это само по себе:
String::toLowerCase;
бессмысленно и действительно Object o = String::toLowerCase;
не компилируется. Лямбды ((args) -> code;
) и ссылки на методы (String::toLowerCase
) не допускаются, если только контекст не сообщает компилятору, какой функциональный интерфейс требуется в контексте, в котором они появляются. Для этого необходимо, чтобы составитель поехал в Австралию и работал за пределами страны. Добавьте тот факт, что Java допускает перегрузку методов, и эта процедура песни и танца между внутренним выводом и внешним выводом становится довольно сложной.
Это причина того, что Java не доводит до бесконечности ни внутренний вывод, ни внешний вывод. Потому что тогда довольно тривиально написать код, выполнение которого потребовало бы год (буквально) компилятору.
Вот теоретически, как Java могла бы это понять, учитывая:
List<Parent> collect = parentList.stream().map(Parent::get).toList();
(Примечание: я упростил ситуацию, используя метод toList()
потока напрямую вместо collect(Collectors.toList()
- не то, чтобы это заработало. Просто, учитывая, что даже это не удается, это легче объяснить).
Тогда компилятору придется сделать некоторый вывод снаружи внутрь: начните с представления, что результат .toList()
присваивается переменной типа List<Parent>
, затем обратите внимание, что знак toList()
равен Stream<T>: List<T> toList()
, мы можем зафиксировать, что T, следовательно, должно быть Parent
, и, следовательно, получатель вызова toList()
(x
в x.method()
называется «получателем») должен быть Stream<Parent>
.
Тогда, следовательно, результат .map(Parent::get)
должен быть Stream<Parent>
. Подпись карты:
Stream<U>: <R> Stream<R> map(Function<? super U, ? extends R> mapper);
(Каждый слой по-своему понимает, что означают дженерики, поэтому, чтобы избежать путаницы, я переименовал используемую здесь букву «T» в U)
Итак, мы блокируем <R>
как <Parent>
, получая следующее:
Stream<U>: Stream<Parent> map(Function<? super U, ? extends Parent> mapper)
И здесь эта часть процесса должна закончиться, потому что мы не можем найти способ продолжить выводить типы для обобщений — какого черта это U? Мы не можем сказать. Давайте просто попробуем Object
, тогда лямбда интерпретируется неправильно (Parent::get
не «подходит» Function<Object, Parent>
, следовательно, неудача).
Итак, идем в другой конец.
parentList
— это List<Parent>
, мы это знаем. У него есть метод stream()
, знак которого равен List<T>: Stream<T>
, поэтому parentList.stream()
имеет тип Stream<Parent>
. Вызывается его метод карты, который выглядит так:
Stream<T>: <R> Stream<R> map(Function<? super T, ? extends R> mapper);
Зафиксировав то, что мы знаем, мы можем превратить это в:
<R> Stream<R> map(Function<? super Parent, ? extends R> mapper);
И на этом все заканчивается. Мы не можем определить R с этой стороны.
Вы можете подумать: Ну, держись! Функция отображения — Parent::get
, которая… говорит вам, что R <? extends Parent>
упрощает это до Parent
, и мы можем продолжить вывод!
Ах, но, чтобы хотя бы интерпретировать то, что Parent::get
должен означать, компилятор должен знать, какой функциональный интерфейс ожидается в его контексте, поэтому он не может просто проверить, что может означать Parent::get
, прежде чем завершить этот шаг. , поэтому, к сожалению, компилятор не может применить этот вывод.
Таким образом, компилятор делает то, что обычно делает, и просто пытается Object
. Это действительно работает: Parent::get
«подходит» Function<Parent, Object>
. (потому что get()
возвращает Родителя, который, безусловно, также является Объектом, так что это нормально). Затем это продолжается, и вы доходите до toList()
, то есть List<Object>
, который затем нельзя присвоить переменной. Но возврат сейчас (возврат к тому моменту, где, очевидно, была допущена ошибка) требует возврата неизвестно на сколько шагов, а javac не собирается этого делать, поэтому ошибка остается, и это именно та ошибка, которую вы в конечном итоге видите. .
Если мы объединим эти два значения, мы сможем «достигнуть цели» — мы можем получить R, выйдя наружу, и T, выйдя наизнанку, и, определив оба, мы сможем завершить работу. Однако это не то, насколько далеко обычно может зайти Java-вывод — объединение результатов двух подходов для определения одного типа слишком сложно. Java определяет, что с помощью вывода наизнанку она не может определить сигнатуры в точке map()
(T можно определить, а R нет), а Java определяет, что с помощью вывода снаружи внутрь она также не может определить это (R можно вычислить, а Т нет). Таким образом, карту невозможно определить, и Java пытается еще раз, просто используя Object
для всех вещей, но это не удается, и вместо этого отображается ошибка компилятора.
Дайте компилятору подсказку, чтобы два подхода встретились таким образом, чтобы не попасть прямо в середину определения одного типа.
Например:
List<Parent> collect = parentList.stream().<Parent>map(Parent::get).toList();
(можете заменить toList
на collect(Collectors.toList())
, всё равно будет работать). Эта «подсказка» позволяет процессу «наизнанку» исправить R (поскольку вы явно сообщаете компилятору, что такое R: Parent
). Ваш подход (сначала присвойте функцию сопоставления переменной типа Function<Parent, Parent>
) тривиально также достигает этой цели.
public class Parent { public <T extends Parent> T get() { ... } }```
Я надеюсь, что этот вопрос слишком упрощен, потому что, как написано, этот код бесполезен/опасен.
Вы хотите:
public Parent get() { ... }
То, что вы написали, говорит: «Существует некоторый тип T, который не известен этому методу и не может быть известен даже во время выполнения. Вызывающий объект выбирает T. Они могут выбрать все, что хотят, при условии, что они выбирают либо Parent, либо что-то, что является подтипом Parent. Этот метод гарантирует, что возвращаемое значение соответствует этому выбору».
Это невозможно. Потому что ты не знаешь, что они выбрали. Все, что вы возвращаете, должно работать для любого выбора, который делает вызывающий абонент, и вы не знаете, какой выбор он сделал. Это означает, что это единственные законные способы написать этот метод:
null
. Всегда. Потому что null
— это все типы сразу.throw
, или бесконечным циклом, или завершением работы JVM. Поскольку вам никогда не придется придумывать возвращаемое значение, вы гарантированно вернете значение типа, которого вы на самом деле не знаете и не можете знать.Или, что более вероятно, вы применяете уродливое приведение: добавляете приведение (T)
и либо принимаете страшное предупреждение компилятора о том, что вы делаете, либо используете @SuppressWarnings
, чтобы избавиться от этого.
Лучший ответ, который я когда-либо видел. Пример, который я написал, представлял собой крайне непрактичный код для понимания этого явления. Какой-то трюк. Большое спасибо за ответ. Хорошего дня.
Чтобы расширить тему «Вызывающий абонент выбирает букву Т»: с помощью объявления public <T extends Parent> T get()
вызывающий абонент может сделать, например: List<String> list = parent.get();
На первый взгляд это может показаться нелогичным, поскольку List<String>
не является подтипом Parent
, однако может существовать тип, расширяющий Parent
и реализующий List<String>
, и вызывающей стороне разрешено выбрать этот гипотетический тип (с последующим расширением его до List<String>
). Вызывающему объекту не нужно доказывать существование такого типа, поскольку обязанностью метода get()
является возврат реализации этого типа.
Я считаю, что написания
stream().<Parent>map
вместоstream().map
будет достаточно, чтобы подсказать компилятору, какой тип вы собираетесь использовать.