Відповіді:
Це в основному спосіб, що дженерики реалізовані в Java за допомогою хитрості компілятора. Складений загальний код насправді використовується лише java.lang.Object
там, де ви говоритеT
(або якийсь інший параметр типу) - і є деякі метадані, щоб сказати компілятору, що він справді є загальним типом.
Коли ви компілюєте якийсь код із загальним типом чи методом, компілятор визначає, що ви дійсно маєте на увазі (тобто, для чого це аргумент типу T
) і під час компіляції перевіряє, що ви робите правильно, але випромінений код знову просто говорить з точки зору java.lang.Object
- компілятор створює додаткові касти, де це необхідно. На час виконання, a List<String>
і a List<Date>
точно однакові; компілятор видалив додаткову інформацію про тип .
Порівняйте це, скажімо, з C #, де інформація зберігається під час виконання, дозволяючи коду містити вирази, такі, typeof(T)
що еквівалентноT.class
- за винятком того, що останні недійсні. (Існують додаткові відмінності між .NET generics і Java generics, зауважте.) Видалення типу є джерелом багатьох "непарних" повідомлень про попередження / помилки під час роботи з Java generics.
Інші ресурси:
Object
(у слабко набраному сценарії) насправді а List<String>
). На Java це просто неможливо - ви можете дізнатися, що це ArrayList
, але не те, що було оригінальним родовим типом. Такі речі можуть виникнути, наприклад, у серіалізації / десеріалізації. Іншим прикладом є те, коли контейнер повинен бути в змозі побудувати екземпляри свого загального типу - ви повинні передавати цей тип окремо в Java (як Class<T>
).
Class<T>
параметр до конструктора (або загального методу) просто тому, що Java не зберігає цю інформацію. Подивіться, EnumSet.allOf
наприклад - аргументу загального типу методу має бути достатньо; чому мені також потрібно вказати "звичайний" аргумент? Відповідь: введіть стирання. Така річ забруднює API. Не цікаво, чи багато ви використовували .NET-дженерики? (продовження)
Так само, як бічна примітка, цікавою є вправа побачити, що компілятор робить, коли виконує стирання - робить усю концепцію трохи легшою для розуміння. Існує спеціальний прапор, за допомогою якого можна передати компілятор для виведення файлів java, у яких були стирані дженерики та вставлені касти. Приклад:
javac -XD-printflat -d output_dir SomeFile.java
Це -printflat
прапор, який передається компілятору, який генерує файли. ( -XD
Частина - це те, що говорить javac
про те, щоб передати її виконуваному баночку, який насправді робить компіляцію, а не просто javac
, але я відхиляюсь ...) Це -d output_dir
необхідно, тому що компілятору потрібно десь помістити нові файли .java.
Це, звичайно, робить більше, ніж просто стирання; всі автоматичні речі, які робить компілятор, виконуються тут. Наприклад, також вставляються конструктори за замовчуванням, нові for
петлі у стилі передбачень розширюються на звичайні for
петлі тощо. Приємно бачити дрібниці, які відбуваються автоматично.
Видалення, буквально означає, що інформація про тип, яка присутня у вихідному коді, стирається із зібраного байтового коду. Давайте зрозуміємо це з деяким кодом.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class GenericsErasure {
public static void main(String args[]) {
List<String> list = new ArrayList<String>();
list.add("Hello");
Iterator<String> iter = list.iterator();
while(iter.hasNext()) {
String s = iter.next();
System.out.println(s);
}
}
}
Якщо скласти цей код і потім декомпілювати його декомпілятором Java, ви отримаєте щось подібне. Зауважте, що декомпільований код не містить сліду інформації про тип, присутній у вихідному вихідному коді.
import java.io.PrintStream;
import java.util.*;
public class GenericsErasure
{
public GenericsErasure()
{
}
public static void main(String args[])
{
List list = new ArrayList();
list.add("Hello");
String s;
for(Iterator iter = list.iterator(); iter.hasNext(); System.out.println(s))
s = (String)iter.next();
}
}
jigawot
сказати, це працює.
Щоб виконати і без того дуже повну відповідь Джона Скіта, ви повинні усвідомити концепцію стирання типу, що випливає з потреби сумісності з попередніми версіями Java .
Спочатку представлена на EclipseCon 2007 (більше не доступна), сумісність включала такі моменти:
Оригінальна відповідь:
Звідси:
new ArrayList<String>() => new ArrayList()
Є пропозиції щодо більшого вдосконалення . Повторіть, що це "Розглядайте абстрактне поняття як реальне", де мовні конструкції повинні бути поняттями, а не лише синтаксичним цукром.
Я також повинен згадати checkCollection
метод Java 6, який повертає динамічно набір безпечного виду зазначеної колекції. Будь-яка спроба вставити елемент неправильного типу призведе до негайного ClassCastException
.
Механізм generics на мові забезпечує перевірку типу компіляції (статичний) типів, але можна перемогти цей механізм за допомогою неперевірених закидів .
Зазвичай це не є проблемою, оскільки компілятор видає попередження про всі такі неперевірені операції.
Однак бувають випадки, коли самої перевірки статичного типу недостатньо, наприклад:
ClassCastException
, що вказує на те, що неправильно набраний елемент був введений у параметризовану колекцію. На жаль, виняток може статися в будь-який час після введення помилкового елемента, тому він, як правило, надає мало інформації про реальне джерело проблеми.Оновлення липня 2012 року, майже через чотири роки:
Він зараз (2012 р.) Детально описаний у " Правилах сумісності API міграції (тест підпису) "
Мова програмування Java реалізує дженерики за допомогою стирання, що забезпечує застарілі та загальні версії, як правило, генерують файли однакових класів, за винятком деякої допоміжної інформації про типи. Бінарна сумісність не порушена, оскільки можна замінити застарілий клас класу на загальний файл класу без зміни або перекомпіляції будь-якого коду клієнта.
Для полегшення взаємодії з негенеричним спадковим кодом також можливе використання стирання параметризованого типу як типу. Такий тип називається необробленим типом ( специфікація мови Java 3 / 4.8 ). Дозвіл необробленого типу також забезпечує зворотну сумісність вихідного коду.
Відповідно до цього, наступні версії
java.util.Iterator
класу сумісні як бінарні, так і вихідні коди:
Class java.util.Iterator as it is defined in Java SE version 1.4:
public interface Iterator {
boolean hasNext();
Object next();
void remove();
}
Class java.util.Iterator as it is defined in Java SE version 5.0:
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
Доповнюючи вже доповнену відповідь Джон Скіт ...
Згадувалося, що реалізація генерики за допомогою стирання призводить до деяких дратівливих обмежень (наприклад, ні new T[42]
). Також було відзначено, що основною причиною таких дій була зворотна сумісність у байт-коді. Це також (переважно) правда. Генерований байт-код -ціль 1.5 дещо відрізняється від лише задекларованого кастингу -цілі 1.4. Технічно навіть можна (через величезну хитрість) отримати доступ до загальних типів даних під час виконання , довівши, що в байт-коді дійсно є щось.
Більш цікавий момент (який не піднімався) полягає в тому, що реалізація генерики за допомогою стирання пропонує трохи більше гнучкості в тому, що може досягти система високого рівня. Хорошим прикладом цього може бути реалізація JVM Scala проти CLR. У JVM можливо реалізувати вищі типи безпосередньо через те, що сам JVM не встановлює обмежень на загальні типи (оскільки ці "типи" фактично відсутні). Це контрастує з CLR, який має знання часу виконання параметрів. Через це сам CLR повинен мати певне поняття, як слід використовувати дженерики, анулюючи спроби розширення системи непередбачуваними правилами. Як результат, вищі типи Scala в CLR реалізуються за допомогою дивної форми стирання, емульованої в самому компіляторі,
Стирання може бути незручним, коли ви хочете робити неслухняні речі під час виконання, але це дає найбільшу гнучкість для авторів-компіляторів. Я здогадуюсь, що це частина того, чому він скоро не піде.
Як я це розумію (будучи хлопцем .NET ), JVM не має поняття generics, тому компілятор замінює параметри типу на Object і виконує весь кастинг для вас.
Це означає, що дженерики Java - це не що інше, як синтаксичний цукор, і не пропонують поліпшення продуктивності для типів значень, які потребують боксу / розпакування при передачі посилання.
Є хороші пояснення. Я лише додаю приклад, щоб показати, як стирання типу працює з декомпілятором.
Оригінальний клас,
import java.util.ArrayList;
import java.util.List;
public class S<T> {
T obj;
S(T o) {
obj = o;
}
T getob() {
return obj;
}
public static void main(String args[]) {
List<String> list = new ArrayList<>();
list.add("Hello");
// for-each
for(String s : list) {
String temp = s;
System.out.println(temp);
}
// stream
list.forEach(System.out::println);
}
}
Декомпільований код з його байтового коду,
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.Consumer;
public class S {
Object obj;
S(Object var1) {
this.obj = var1;
}
Object getob() {
return this.obj;
}
public static void main(String[] var0) {
ArrayList var1 = new ArrayList();
var1.add("Hello");
// for-each
Iterator iterator = var1.iterator();
while (iterator.hasNext()) {
String string;
String string2 = string = (String)iterator.next();
System.out.println(string2);
}
// stream
PrintStream printStream = System.out;
Objects.requireNonNull(printStream);
var1.forEach(printStream::println);
}
}
Навіщо використовувати Generices
У двох словах, дженерики дозволяють типам (класам та інтерфейсам) бути параметрами при визначенні класів, інтерфейсів та методів. Так само, як і більш звичні формальні параметри, які використовуються в деклараціях методів, параметри типу надають спосіб повторного використання одного і того ж коду з різними входами. Різниця полягає в тому, що вхідні дані до формальних параметрів є значеннями, тоді як вхідними даними параметри є типи. Ода, яка використовує дженерики, має чимало переваг порівняно з неотричним кодом:
Що таке стирання типу
Generics були представлені на мові Java для забезпечення більш чітких перевірок типу під час компіляції та для підтримки загального програмування. Для реалізації дженерики компілятор Java застосовує стирання типу для:
[NB] -Що таке метод мостів? Коротше: у випадку параметризованого інтерфейсу, такого як Comparable<T>
, це може спричинити додавання компілятора додаткових методів; ці додаткові методи називаються мостами.
Як працює стирання
Стирання типу визначається наступним чином: видаліть усі параметри типу з параметризованих типів і замініть будь-яку змінну типу на стирання її зв'язаної, або на Object, якщо вона не має меж, або зі стиранням крайньої лівої межі, якщо вона має кілька меж. Ось кілька прикладів:
List<Integer>
, List<String>
і List<List<String>>
єList
.List<Integer>[]
єList[]
.List
само по собі, як і для будь-якого сирого типу.Integer
само по собі, подібно до будь-якого типу без параметрів типу.T
у визначенні asList
є Object
, тому щоT
не має меж.T
у визначенні max
є Comparable
, тому T
що зв'язанеComparable<? super T>
.T
остаточному визначенні стирання max
є Object
, тому
T
що обмежило Object
&, Comparable<T>
а ми беремо стирання крайньої лівої межі.Потрібно бути обережними при використанні дженериків
У Java два різні методи не можуть мати однаковий підпис. Оскільки дженерики реалізовані шляхом стирання, також випливає, що два різних методу не можуть мати підписи з одним стиранням. Клас не може перевантажувати два методи, підписи яких мають одне і те ж стирання, і клас не може реалізувати два інтерфейси, які мають одне і те ж стирання.
class Overloaded2 {
// compile-time error, cannot overload two methods with same erasure
public static boolean allZero(List<Integer> ints) {
for (int i : ints) if (i != 0) return false;
return true;
}
public static boolean allZero(List<String> strings) {
for (String s : strings) if (s.length() != 0) return false;
return true;
}
}
Ми плануємо, щоб цей код працював так:
assert allZero(Arrays.asList(0,0,0));
assert allZero(Arrays.asList("","",""));
Однак у цьому випадку стирання підписів обох методів однакові:
boolean allZero(List)
Тому зіткнення імені повідомляється під час компіляції. Не можна давати обом методам однакову назву і намагатися розрізнити їх перевантаженням, оскільки після стирання неможливо відрізнити один виклик методу від іншого.
Сподіваємось, читач сподобається :)