Чому деякі стверджують, що реалізація дженерики Java не є поганою?


115

Я періодично чув, що з дженериками, Java не зрозуміла. (найближча довідка, тут )

Пробачте мою недосвідченість, але що б зробило їх краще?


23
Я не бачу причин закривати це; з усіма лайнами навколо генерики, дещо з'ясування різниць між Явою та C # може допомогти людям, які намагаються ухилитися від невідомого занедбання.
Роб

Відповіді:


146

Погано:

  • Інформація про тип втрачається під час компіляції, тому під час виконання ви не можете сказати, яким типом це "малося"
  • Не можна використовувати для типів значень (це biggie - в .NET a List<byte>дійсно підтримується, byte[]наприклад, бокс не потрібен)
  • Синтаксис виклику загальних методів відстій (IMO)
  • Синтаксис обмежень може заплутатись
  • Уайлдкард взагалі заплутаний
  • Різні обмеження через вищезазначене кастинг тощо

Добре:

  • Підстановка підказки дозволяє визначати коваріацію / протиріччя на стороні виклику, що дуже акуратно в багатьох ситуаціях
  • Це краще, ніж нічого!

51
@Paul: Я думаю, що я б віддав перевагу тимчасовому поломці, але гідному API після цього. Або скористайтеся маршрутом .NET і введіть нові типи колекцій. (.NET generics в 2.0 не зламав 1.1 додатків.)
Джон Скіт

6
Маючи велику існуючу кодову базу, мені подобається те, що я можу почати змінювати підпис одного методу, щоб використовувати дженерики, не змінюючи всіх, хто його викликає.
Пол Томблін

38
Але недоліки цього величезні та постійні. Існує велика короткострокова виграш та довгострокова втрата ІМО.
Джон Скіт

11
Джон - "Це краще, ніж нічого" суб'єктивно :)
MetroidFan2002

6
@Rogerio: Ви стверджували, що мій перший елемент "Погано" невірний. Мій перший пункт "Погано" говорить, що інформація втрачається під час компіляції. Щоб це було невірно, ви повинні сказати, що інформація не втрачається під час компіляції ... Що стосується плутанини інших розробників, ви, здається, єдині збентежені тим, що я говорю.
Джон Скіт

26

Найбільша проблема полягає в тому, що дженерики Java - це лише час компіляції, і ви можете підривати її під час виконання. C # хвалить, тому що він більше перевіряє час виконання. У цій публікації є дійсно хороша дискусія , і вона посилається на інші дискусії.


Це насправді не проблема, це ніколи не робилося для виконання. Начебто ви кажете, що човни - це проблема, оскільки вони не можуть підніматися на гори. Як мова, Java робить те, що потрібно багатьом людям: висловлювати типи змістовно. Під час виконання інструментів під час виконання люди все ще можуть передавати Classоб'єкти навколо.
Вінсент Кантін

1
Він правий. ваша анологія неправильна, тому що люди, які проектували човен, ДОБРІЛЬНО спроектували його підніматися на гори. Їх цілі дизайну були не дуже добре продумані для того, що було потрібно. Зараз ми всі застрягли лише на човні, і ми не можемо піднятися на гори.
WiegleyJ

1
@VincentCantin - це, безумовно, проблема. Отже, ми всі скаржимось на це. Java-дженерики напівфабрикати.
Джош М.

17

Основна проблема полягає в тому, що Java насправді не має дженериків під час виконання. Це час компіляції.

Під час створення загального класу в Java вони використовують метод під назвою "Тип стирання", щоб фактично видалити всі загальні типи з класу і по суті замінити їх на Object. Версія загальної версії для дженерики полягає в тому, що компілятор просто вставляє касти у вказаний загальний тип кожного разу, коли він з’являється в тілі методу.

Це має багато недоліків. Одним з найбільших, IMHO, є те, що ви не можете використовувати рефлексію для огляду родового типу. Типи насправді не є загальними в байтовому коді, і тому їх не можна перевіряти як генеричні.

Чудовий огляд відмінностей тут: http://www.jprl.com/Blog/archive/development/2007/Aug-31.html


2
Я не впевнений, чому ви були голосовані. Як я розумію, ви праві, і це посилання є дуже інформативним. Вгору проголосували.
Джейсон Джексон

@Jason, спасибі У моїй оригінальній версії був помилковий помилок, я вважаю, що мене за це обрали.
JaredPar

3
Помилка, тому що ви можете використовувати відображення для перегляду родового типу, підпису родового методу. Ви просто не можете використовувати відображення для перевірки загальної інформації, пов'язаної з параметризованим екземпляром. Наприклад, якщо у мене є клас з методом foo (Список <String>), я можу рефлексивно знайти "String"
oxbow_lakes

Існує маса статей, в яких сказано, що стирання типу робить неможливим пошук фактичних параметрів типу. Скажемо: Об'єкт x = новий X [Y] (); чи можете ви показати, як, задавши х, знайти тип Y?
user48956

@oxbow_lakes - використовувати рефлексію для пошуку родового типу майже так само погано, як і не мати можливості цього зробити. Наприклад, це погане рішення.
Джош М.

14
  1. Реалізація виконання (тобто не стирання типу);
  2. Можливість використання примітивних типів (це пов'язано з (1));
  3. У той час як wildcarding корисний синтаксис, і знати, коли його використовувати - це те, що спотикає багато людей. і
  4. Поліпшення продуктивності не відбувається (через (1); дженерики Java - синтаксичний цукор для об'єктів кастингу).

(1) призводить до дуже дивної поведінки. Найкращий приклад, про який я можу придумати, - це. Припустимо:

public class MyClass<T> {
  T getStuff() { ... }
  List<String> getOtherStuff() { ... }
}

потім оголосити дві змінні:

MyClass<T> m1 = ...
MyClass m2 = ...

Тепер зателефонуйте getOtherStuff():

List<String> list1 = m1.getOtherStuff(); 
List<String> list2 = m2.getOtherStuff(); 

Другий аргумент загального типу позбавлений компілятором, оскільки це необроблений тип (тобто параметризований тип не надається), хоча він не має нічого спільного з параметризованим типом.

Я також згадаю мою улюблену декларацію від JDK:

public class Enum<T extends Enum<T>>

Крім диких карт (це мішана сумка) я просто думаю, що .Net generics є кращими.


Але компілятор підкаже вам, особливо з варіантом мастила сировини.
Том Хотін - тайклін

Вибачте, чому останній рядок - помилка компіляції? Я псуюся з ним у Eclipse і не можу змусити його там провалитися - якщо я додам достатньо речей для решти його для компіляції.
oconnor0

public class Redundancy<R extends Redundancy<R>>;)
java.is.for.desktop

@ oconnor0 Це не збій компіляції, а попередження компілятора без видимих ​​причин:The expression of type List needs unchecked conversion to conform to List<String>
artbristol

Enum<T extends Enum<T>>спочатку може здатися дивним / зайвим, але насправді досить цікавим, принаймні в рамках обмежень Java / це generics. Енуми мають статичний values()метод, який дає масив їх елементів, введених як enum, а не Enum, і цей тип визначається загальним параметром, тобто ви хочете Enum<T>. Звичайно, це типування має сенс тільки в контексті перераховується типу, і всі перерахування є підкласами Enum, що ви хочете Enum<T extends Enum>. Однак Java не любить змішувати необроблені типи з загальними, тобто Enum<T extends Enum<T>>для послідовності.
JAB

10

Я збираюся викинути дійсно суперечливу думку. Дженріки ускладнюють мову та ускладнюють код. Наприклад, скажімо, що у мене є карта, яка відображає рядок до списку рядків. За старих часів я міг це оголосити просто як

Map someMap;

Тепер я маю оголосити це як

Map<String, List<String>> someMap;

І кожен раз, коли я передаю це в якийсь метод, я мушу повторювати цю велику довгу заяву знову і знову. На мою думку, все те, що зайве набирає тексту, відволікає розробника і виводить його із «зони». Крім того, коли код наповнений великою кількістю суворості, іноді важко повернутися до нього пізніше і швидко просіяти всю суть, щоб знайти важливу логіку.

У Java вже є погана репутація, що вона є однією з найбільш багатослівних мов у загальному користуванні, і генеричні файли просто додають до цієї проблеми.

А що ви насправді купуєте за всю цю зайву багатослівність? Скільки разів у вас виникли проблеми, коли хтось помістив Integer у колекцію, яка повинна містити Strings, або де хтось намагався витягнути String із колекції Integers? За мій 10-річний досвід роботи над створенням комерційних програм Java, це ніколи не було великим джерелом помилок. Отже, я не дуже впевнений, що ви отримуєте для додаткової багатослівності. Це дійсно просто вражає мене додатковим бюрократичним багажем.

Тепер я вийду по-справжньому суперечливим. Я вважаю найбільшою проблемою колекцій у Java 1.4 необхідність скрізь друкувати. Я розглядаю ці шрифти як додаткові, багатослівні, які мають багато тих же проблем, що і генеричні. Так, наприклад, я не можу просто зробити

List someList = someMap.get("some key");

я маю зробити

List someList = (List) someMap.get("some key");

Причина, звичайно, в тому, що get () повертає Об'єкт, який є супертипом Список. Таким чином, призначення не може бути виконано без набору тексту. Знову ж таки, подумайте, наскільки насправді купує це правило. З мого досвіду, не багато.

Я думаю, що у Java було б набагато краще, якби 1) вона не додала дженерики, але 2) натомість дозволила мати неявне кастинг із супертипу на підтип. Нехай неправильні касти будуть спіймані під час виконання. Тоді я міг би мати простоту визначення

Map someMap;

а пізніше робити

List someList = someMap.get("some key");

вся сукупність пішла б, і я дійсно не думаю, що я б вводив нове велике джерело помилок у свій код.


15
Вибачте, я не згоден майже з усім, що ви сказали. Але я не збираюся його проголосувати, тому що ви це добре стверджуєте.
Пол Томблін

2
Звичайно, є маса прикладів великих, успішних систем, побудованих на таких мовах, як Python та Ruby, які роблять саме те, що я запропонував у своїй відповіді.
Клінт Міллер

Я думаю, що все моє заперечення проти такого типу ідей полягає не в тому, що це погана ідея сама по собі, а в тому, що, оскільки сучасні мови, такі як Python і Ruby, полегшують життя і простіше для розробників, розробники, в свою чергу, стають інтелектуально поступливими і зрештою мають нижчу осмислення власного коду.
Дан Дао

4
"А що ви насправді купуєте за всю цю зайву багатослівність? ..." Це розповідає поганому програмісту технічного обслуговування, що має бути в цих колекціях. Я вважав, що це проблема, яка працює з комерційними додатками Java.
richj

4
"Скільки разів у вас виникли проблеми, коли хтось помістив [x] у колекцію, яка повинна вмістити [y] s?" - О, хлопче, я програв! Також навіть якщо помилки немає, це вбивця для читання. І сканування багатьох файлів, щоб дізнатись, яким буде об’єкт (або налагодження), насправді виводить мене із зони. Можливо, вам сподобається Haskell - навіть сильний набір тексту, але менш чіткий (тому що виводяться типи).
Martin Capodici

8

Іншим побічним ефектом, який полягає в тому, що вони складають час компіляції, а не час виконання, - це те, що ви не можете викликати конструктор загального типу. Тож ви не можете використовувати їх для впровадження загальної фабрики ...


   public class MyClass {
     public T getStuff() {
       return new T();
     }
    }

--jeffk ++


6

Java-дженерики перевіряються на правильність під час компіляції, а потім видаляється вся інформація про тип (процес називається стиранням типу . Таким чином, загальна інформація List<Integer>буде зведена до її необробленого типу , негенеральногоList , який може містити об'єкти довільного класу.

Це призводить до можливості вставлення до списку довільних об'єктів до списку під час виконання, а також неможливо сказати, які типи використовувались як загальні параметри. Останнє в свою чергу призводить до

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if(li.getClass() == lf.getClass()) // evaluates to true
  System.out.println("Equal");

5

Я б хотів, щоб це була вікі, щоб я міг додати інших людей ... але ...

Проблеми:

  • Тип стирання (відсутність часу виконання)
  • Немає підтримки для примітивних типів
  • Незмінність з анотаціями (обидва вони були додані в 1,5. Я все ще не впевнений, чому анотації не дозволяють генерікам окрім того, щоб не поспішати функцій)
  • Незмінність з масивами. (Іноді мені дуже хочеться робити щось на зразок Class <? Розширює MyObject >[], але мені це не дозволено)
  • Синтаксис та поведінка підкреслених символів
  • Справа в тому, що загальна підтримка непостійна в усіх класах Java. Вони додавали його до більшості методів колекції, але раз у раз ви натрапляєте на екземпляр, де його немає.

5

Ігноруючи стирання стирання всього типу, дженерики, як зазначено, просто не працюють.

Це компілює:

List<Integer> x = Collections.emptyList();

Але це синтаксична помилка:

foo(Collections.emptyList());

Де foo визначається як:

void foo(List<Integer> x) { /* method body not important */ }

Отже, чи перевірка типу виразу залежить від того, призначається вона локальній змінній або фактичному параметру виклику методу. Як божевільний це?


3
Невідповідний висновок javac - це лайно.
мП.

3
Я вважаю, що причина відхилення останньої форми полягає в тому, що через перевантаження методу може бути більше однієї версії "foo".
Джейсон Крейтон

2
ця критика більше не застосовується, оскільки Java 8 представила вдосконалене умовиводство для цільового типу
oldrinb

4

Впровадження генерики в Java було складним завданням, оскільки архітектори намагалися збалансувати функціональність, простоту використання та зворотну сумісність із застарілим кодом. Цілком очікувано, що потрібно було піти на компроміси.

Є й такі, хто також вважає, що реалізація дженериків Java збільшила складність мови до неприйнятного рівня (див. " Генерики вважаються шкідливими "). Часті запитання про дженерики Angelika Langer дає досить гарне уявлення про те, як складні речі можуть стати.


3

Java не застосовує Generics під час виконання, лише під час компіляції.

Це означає, що ви можете робити цікаві речі, такі як додавання неправильних типів до загальних колекцій.


1
Компілятор скаже вам, що ви накрутили, якщо вам це вдається.
Том Хотін - тайклін

@Tom - не обов’язково. Є способи обдурити компілятор.
Пол Томблін

2
Ви не слухаєте те, що вам каже компілятор ?? (див. мій перший коментар)
Tom Hawtin - tackline

1
@Tom, якщо у вас є ArrayList <String> і ви передаєте його методу старого стилю (скажімо, застарілий або код третьої сторони), який бере простий список, цей метод може потім додати до списку все, що йому подобається. Якщо це буде застосовано під час виконання, ви отримаєте виняток.
Пол Томблін

1
static void addToList (Список списку) {list.add (1); } Список <String> list = новий ArrayList <String> (); addToList (список); Це компілює і навіть працює під час виконання. Єдиний раз, коли ви бачите проблему, це коли ви видаляєте зі списку очікуючи рядок і отримуєте int.
Лаплі Андерсон

3

Java-дженерики є лише компільованим часом і збираються в негенеричний код. У C # фактично складений MSIL є загальним. Це має величезні наслідки для продуктивності, тому що Java все ще працює під час виконання. Дивіться тут докладніше .


2

Якщо ви слухаєте Java Posse № 279 - Інтерв'ю з Джо Дарсі та Алексом Баклі , вони говорять про це питання. Це також посилання на публікацію в блозі Ніла Гафтера під назвою Reified Generics for Java, яка говорить:

Багато людей незадоволені обмеженнями, спричиненими способом впровадження дженериків на Java. Зокрема, вони незадоволені тим, що параметри загального типу не повторно змінюються: вони недоступні під час виконання. Загальна версія реалізована за допомогою стирання, в якому параметри загального типу просто видаляються під час виконання.

Ця публікація в блозі посилається на старішу статтю " Загадка через стирання: відповідь" , в якій наголошувалося на сумісності міграцій у вимогах.

Метою було забезпечення зворотної сумісності як вихідного, так і об'єктного коду, а також сумісності міграції.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.