Java неявно преобразует List<String> в List<Long> с использованием дженериков

Я пытался запустить приведенный ниже код

import java.util.Arrays;
import java.util.List;

class HelloWorld {
    
    
    public static void main(String[] args){
    ttt();
  }

  public static void ttt(){
      
    List<String> result;
    result = kkk();
    // String ll = result.get(0);
    // ll.split(",");
    if (result.get(0) instanceof String){
      System.out.println("alsfjalsdfjaslkfj");
    }else{
      System.out.println("Long Long");

    }
    System.out.println(result);
  }

  public static <R> R kkk(){
    List<Long> x = Arrays.asList(2L, 2L, 3L);
    return (R) x;
  }
}

Я ожидал, что код выйдет из строя во время компиляции. Удивительно, но код запускает печать «Long Long». Как Java может загружать List<Long> в List<String>. Есть ли какая-либо документация об этом поведении?

Обобщенные шаблоны в Java работают не так, как в C#. Прочтите документацию. Особое внимание уделите стиранию типов.

k314159 30.07.2024 18:14

ты в конце концов проигнорировал предупреждение о «непроверенном приведении»?! (на строке return (R) x;)

user85421 30.07.2024 19:10

Это старый и хорошо известный трюк, который имеет несколько вариаций. Поздравляем, что вы это обнаружили. Вы также можете сделать, например List<String> result = (List<String>) (List<?>) List.of(2L, 2L, 3L);.

Anonymous 30.07.2024 20:11
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
3
79
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Дженерики — плод воображения компилятора: JVM понятия не имеет, что такое дженерики. Большинство дженериков удаляются; те немногие обобщения, которые не являются таковыми, являются комментариями к JVM: JVM (java.exe) понятия не имеет, что это значит, и просто пропускает его во время анализа файла класса. Те немногие дженерики, которые существуют в файлах классов, существуют исключительно для пользы javac, который, в конце концов, может использовать файлы .class как часть входных данных (чтобы знать, какие библиотеки существуют и могут быть вызваны для кода, который вы на самом деле компилируете).

Стирание?

Тривиальный пример стирания типа :

Object o = new ArrayList<String>();
printStringGiven(o);

Здесь нет ничего, для чего вы могли бы написать printStringGiven — никакой код не сможет «извлечь» String из o здесь. Потому что оно стерто. Попробуйте — скомпилируйте, запустите javap -c -p, чтобы увидеть байт-код. От этого ничего не осталось. Никакого упоминания String нигде во всем class файле.

Вот почему вы получаете это предупреждение при компиляции кода: приведение к (R) компилируется буквально ни к чему — это подсказка компилятора. (R) здесь означает: «Да, javac, заткнись. Я знаю, что намеренно мешаю твоей способности гарантировать, что все дженерики имеют смысл, и поэтому ты больше не можешь этого гарантировать».

Так что же происходит во время выполнения?

Java не знает, что такое дженерики. Совсем. Следовательно, это:

List<String> list = new ArrayList<String>();
list.add("Hello");
String x = list.get(0);
x.toLowerCase();

тогда это казалось бы принципиально невозможным: среда выполнения понятия не имеет, что такое дженерики, поэтому это просто вещи, которые являются List (а не List<String>, следовательно, list.get(0) будет типом Object, и, следовательно, вызов toLowerCase() на нем является нарушением верификатора класса и файл класса даже не должен быть загружен.

Однако этого не происходит. javac осознает, что среда выполнения понятия не имеет, что такое дженерики. Следовательно, Java автоматически внедрит приведение, приведенное выше компилируется идентично:

List list = new ArrayList();
list.add("Hello");
String x = (String) list.get(0);
x.toLowerCase();

И это, в частности, включает в себя ClassCastException, который вы получите, если приведение завершится неудачей (помните, приведение, если только тип в скобках не является примитивом, ничего не преобразует. Оно либо ничего не делает, либо приводит к ClassCastException) . Итак, да, Java может генерировать ClassCastExceptions в строках кода, которые включают нулевое приведение в вашем файле java (потому что компилятор внедряет их, чтобы заставить работать дженерики).

Почему, как глупо?

Нисколько. Поскольку дженерики являются плодом воображения компилятора, подстановочные знаки могут существовать как основная часть языка, и это экономит массу места в куче, потому что по-прежнему остается только один java.util.List вместо, например. C, где обобщены обобщения, где в куче должно быть некоторое пространство для представления каждого списка, который вы используете в коде C - List<String>, List<Integer>, List<Number>, List<? extends Number> и так далее. См. эту публикацию в блоге известного основного инженера команды OpenJDK для получения дополнительной информации — обратите внимание: вам нужно довольно глубокое понимание принципов языкового проектирования, чтобы следить за мыслительными процессами.

Отличный ответ. Одна мелочь: скомпилированный код не хранится в куче. Следовательно, наличие нескольких реифицированных версий List ничего не добавит в кучу, а только в ту часть памяти, где хранится скомпилированный код (имя которого различается в зависимости от языка и платформы).

k314159 30.07.2024 18:29

Что ж, «permgen» давно исчез, и сами файлы классов также живут в куче. Если бы можно было написать if (o.getClass() == List<Integer>.class), концепция List<Integer> должна была бы чем-то быть. Сам объект o нужно где-то хранить Integer.class. И это может быть Map<List<? extends Foo<T>>, V>, так что это не так просто, как указатель на экземпляр j.l.Class.

rzwitserloot 30.07.2024 18:45

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