Ссылка на метод и вывод типа в Java 8

Я столкнулся с проблемой со ссылкой на метод и выводом типа.

Родительский класс, который возвращает тип 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?

Я считаю, что написания stream().<Parent>map вместо stream().map будет достаточно, чтобы подсказать компилятору, какой тип вы собираетесь использовать.

khelwood 17.04.2024 15:29

может быть public Parent get() { ... }, или вам действительно нужен метод, возвращающий другой тип?

user85421 17.04.2024 15:40

Обратите внимание, что подпись этого метода немного странная, поскольку подразумевает, что реализация может угадать тип возвращаемого значения, который ожидает вызывающая сторона (или вызывающая сторона каким-то образом знает, что получить во время компиляции). Единственный способ реализовать такой метод — это непроверяемое приведение типов. Вы уверены, что это настоящая подпись? Я бы посчитал это плохой практикой, и в прошлом я видел худшие проблемы с подобными методами.

Didier L 19.04.2024 16:17
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
3
90
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вывод обобщений не охватывает бесконечное пространство вызовов связанных методов.

Необходимые базовые знания: вывод Java сжигает свечу с обоих концов

Компилятор выполняет как внутренний, так и внешний вывод типа.

Наизнанку — это стандартный подход, используемый компилятором. Наизнанку — это вывод, скажем:

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() - не то, чтобы это заработало. Просто, учитывая, что даже это не удается, это легче объяснить).

Конец свечи 1: снаружи внутрь.

Тогда компилятору придется сделать некоторый вывод снаружи внутрь: начните с представления, что результат .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>, следовательно, неудача).

Конец свечи 2: Наизнанку

Итак, идем в другой конец.

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, чтобы избавиться от этого.

Лучший ответ, который я когда-либо видел. Пример, который я написал, представлял собой крайне непрактичный код для понимания этого явления. Какой-то трюк. Большое спасибо за ответ. Хорошего дня.

kangworld 18.04.2024 14:29

Чтобы расширить тему «Вызывающий абонент выбирает букву Т»: с помощью объявления public <T extends Parent> T get() вызывающий абонент может сделать, например: List<String> list = parent.get(); На первый взгляд это может показаться нелогичным, поскольку List<String> не является подтипом Parent, однако может существовать тип, расширяющий Parent и реализующий List<String>, и вызывающей стороне разрешено выбрать этот гипотетический тип (с последующим расширением его до List<String> ). Вызывающему объекту не нужно доказывать существование такого типа, поскольку обязанностью метода get() является возврат реализации этого типа.

Holger 18.04.2024 19:06

Другие вопросы по теме