Я пытался запустить приведенный ниже код
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>
. Есть ли какая-либо документация об этом поведении?
ты в конце концов проигнорировал предупреждение о «непроверенном приведении»?! (на строке return (R) x;
)
Это старый и хорошо известный трюк, который имеет несколько вариаций. Поздравляем, что вы это обнаружили. Вы также можете сделать, например List<String> result = (List<String>) (List<?>) List.of(2L, 2L, 3L);
.
Дженерики — плод воображения компилятора: 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
ничего не добавит в кучу, а только в ту часть памяти, где хранится скомпилированный код (имя которого различается в зависимости от языка и платформы).
Что ж, «permgen» давно исчез, и сами файлы классов также живут в куче. Если бы можно было написать if (o.getClass() == List<Integer>.class)
, концепция List<Integer> должна была бы чем-то быть. Сам объект o
нужно где-то хранить Integer.class
. И это может быть Map<List<? extends Foo<T>>, V>
, так что это не так просто, как указатель на экземпляр j.l.Class
.
Обобщенные шаблоны в Java работают не так, как в C#. Прочтите документацию. Особое внимание уделите стиранию типов.