Що не так з дженериками Java? [зачинено]


49

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




як Клінтон Бег ("хлопець iBatis") сказав, "вони не працюють ..."
гнат

Відповіді:


54

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


4
Просто додати, через стирання типу, отримання загальної інформації під час виконання неможливо, якщо ви не використовуєте щось на зразок TypeLiteral google-guice.googlecode.com/svn/trunk/javadoc/com/google/inject/…
Jeremy Heiler

43
Це означаєnew ArrayList<String>.getClass() == new ArrayList<Integer>.getClass()
Зауважте, що потрібно самостійно подумати над ім'ям

9
@Job ні, це не так. І C ++ використовує абсолютно інший підхід до метапрограмування, що називається шаблонами. Він використовує типи качок, і ви можете робити такі корисні речі, як, template<T> T add(T v1, T v2) { return v1->add(v2); }і там у вас є по-справжньому загальний спосіб створення функції, яка має addдві речі, незалежно від того, що це, вони просто повинні мати метод, названий add()одним параметром.
Тринідад

7
@Job, це дійсно генерований код. Шаблони призначені для заміни препроцесора C, і для цього вони повинні мати можливість робити дуже розумні хитрощі і самі по собі є повною мовою Тьюрінга. Java generics - це лише цукор для безпечних для контейнерів типів, а C # generics є кращим, але все-таки шаблон C ++ для бідної людини
Тринідад

3
@Job AFAIK, дженерики Java не генерують клас для кожного нового загального типу, вони просто додають typecasts до коду, який використовує загальні методи, тому це насправді не метапрограмування IMO. Шаблони C # генерують новий код для кожного загального типу, тобто якщо ви використовуєте Список <int> та Список <double> у світі C #, генеруватиме код для кожного з них. На відміну від шаблонів, C # generic все ще вимагає знати, до якого типу ви можете його подати. Ви не можете реалізувати простий Addприклад, який я наводив, немає жодного способу, не знаючи заздалегідь, до яких класів можна застосувати, що є недоліком.
Тринідад

26

Зауважте, що відповіді, які вже були надані, зосереджені на поєднанні мови Java, JVM та бібліотеки класів Java.

Що стосується мови Java, то нічого спільного з дженериками Java немає. Як описано в C # vs Java generics , дженерики Java вкрай чудові на рівні мови 1 .

Субоптимальним є те, що JVM не підтримує дженерики безпосередньо, що має кілька основних наслідків:

  • частини бібліотеки класів, пов'язані з відображенням, не можуть розкрити інформацію про повний тип, яка була доступна на мові Java
  • передбачається певне виконання покарання

Я вважаю, що розрізнення, яке я роблю, є педантичним, оскільки мова мови є всюдисущо складеною з JVM як цільової та Java Class Library як основної бібліотеки.

1 За можливим винятком підстановок, які, як вважають, роблять висновок про тип нерозбірливим у загальному випадку. Це головна різниця між C # і Java generic, що не згадується дуже часто. Спасибі, Сурма.


14
JVM не потребує підтримки дженериків. Це було б те саме, що говорити, що у C ++ немає шаблонів, оскільки реальна машина ix86 не підтримує шаблони, що хибно. Проблема полягає у підході компілятора до впровадження загальних типів. І знову ж таки, шаблони C ++ не передбачають штрафних санкцій під час запуску, взагалі ніяких роздумів не робиться, я думаю, що єдиною причиною, яку потрібно було б зробити на Java, є лише поганий дизайн мови.
Тринідад

2
@Trinidad, але я не сказав, що у Java немає дженериків, тому я не бачу, як це те саме. І так, JVM не потребує їх підтримки, як і C ++ не потребує оптимізації коду. Але не оптимізуючи це, безумовно, вважатимуться як "щось не так".
Роман Старков

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

4
@Trinidad: У C ++, якщо припустити, що компілятору надано достатньо часу та пам’яті, шаблони можна використовувати, щоб робити все, що завгодно, і все, що можна зробити, з інформацією, яка є доступною під час компіляції (оскільки компіляція шаблонів завершена Тьюрінгом), але є не можна створювати шаблони, використовуючи інформацію, яка недоступна до запуску програми. У .net, навпаки, програма може створювати типи на основі введення; було б досить легко написати програму, де кількість різних типів, які можна було б створити, перевищує кількість електронів у Всесвіті.
supercat

2
@Trinidad: Очевидно, що жодне єдине виконання програми не могло б створити всі ці типи (або, дійсно, нічого, що перевищує їх нескінченну малу частину), але справа в тому, що цього не потрібно. Потрібно лише створити типи, які фактично використовуються. Можна мати програму, яка могла б прийняти будь-яке довільне число (наприклад 8675309) і з неї створити тип, унікальний для цього числа (наприклад Z8<Z6<Z7<Z5<Z3<Z0<Z9>>>>>>>), який би мав різні члени від будь-якого іншого типу. У C ++ всі типи, які можна було б генерувати з будь-якого введення, повинні були бути створені під час компіляції.
supercat

13

Звичайна критика - це відсутність вдосконалення. Тобто об’єкти під час виконання не містять інформації про їх загальні аргументи (хоча інформація все ще присутня у полях, методах, конструкторах та розширеному класі та інтерфейсах). Це означає, що ви могли б кинути, скажімо, ArrayList<String>на List<File>. Компілятор подасть вам попередження, але він також попередить вас, якщо ви ArrayList<String>присвоїте його Objectпосиланням, а потім подасте на нього List<String>. Переваги полягають у тому, що з дженериками ви, мабуть, не повинні робити кастинг, продуктивність краща без зайвих даних і, звичайно, зворотної сумісності.

Деякі люди скаржаться, що ви не можете перевантажуватись на основі загальних аргументів ( void fn(Set<String>)і void fn(Set<File>)). Вам потрібно використовувати кращі назви методів. Зауважте, що ця перевантаження не потребуватиме повторних змін, оскільки перевантаження є статичною проблемою під час компіляції.

Первісні типи не працюють з дженериками.

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


"Зауважте, що ця перевантаження не потребує переробки, оскільки перевантаження є статичною проблемою, яка складена під час компіляції." Чи не так? Як би це працювало без переробки? Чи не повинен JVM знати під час виконання, який тип набору у вас є, щоб знати, який метод викликати?
MatrixFrog

2
@MatrixFrog Ні. Як я вже кажу, перевантаження - це проблема часу компіляції. Компілятор використовує статичний тип виразу для вибору певної перевантаження, яка потім є методом, записаним у файл класу. Якщо у вас є Object cs = new char[] { 'H', 'i' }; System.out.println(cs);друк дурниць. Зміна типу csдо char[]і ви отримаєте Hi.
Том Хотін - тайклін

10

Java generics смоктати, тому що ви не можете зробити наступне:

public class AsyncAdapter<Parser,Adapter> extends AsyncTask<String,Integer,Adapter> {
    proptected Adapter doInBackground(String... keywords) {
      Parser p = new Parser(keywords[0]); // this is an error
      /* some more stuff I was hoping for but couldn't do because
         the compiler wouldn't let me
      */
    }
}

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


Вам не потрібно врубати кожен клас. Вам просто потрібно додати відповідний заводський клас і ввести його в AsyncAdapter. (І в принципі мова йде не про дженерики, а про те, що конструктори не успадковуються в Java).
Пітер Тейлор

13
@PeterTaylor: Підходящий заводський клас просто пересуває всю плиту котла в якийсь інший безлад, і це все ще не вирішує проблему. Тепер кожен раз, коли мені хочеться створити новий екземпляр, Parserмені потрібно зробити ще більше закручений заводський код. Тоді якби "загальне" по-справжньому означало загальне, то не було б потреби в фабриках і в усіх інших дурницях, що випливає з назви дизайнерських моделей. Це одна з багатьох причин, які я вважаю за краще працювати в динамічних мовах.
davidk01

2
Іноді подібне в C ++, де ви не можете передати адресу конструктора, коли використовуєте шаблони + віртуали - ви можете просто використовувати заводський функтор.
Марк К Коуан

Java смокче, тому що ви не можете написати "захищений" перед назвою методу? Бідні ви. Дотримуйтесь динамічних мов. І будьте особливо обережні до Haskell.
fdreger

5
  • Попередження компілятора щодо відсутніх загальних параметрів, які не служать ніякій меті, роблять мову безглуздо багатослівною, наприклад public String getName(Class<?> klazz){ return klazz.getName();}

  • Generics не грає добре з масивами

  • Інформація про втрачений тип робить відображення безладом лиття та клейкої стрічки.


Мені дратується, коли HashMapзамість цього я отримую попередження про використання HashMap<String, String>.
Майкл К

1
@Michael, але це насправді слід використовувати з дженериками ...
альтернатива

Проблема масиву переходить до повторюваності. Пояснення див. У книзі Java Generics (Alligator).
ncmathsadist

5

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

List<String> arr = new ArrayList<String>();
assertTrue( ArrayList.class, arr.getClass() );
TypeVarible[] types = arr.getClass().getTypedVariables();

На жаль, повернені типи не можуть сказати вам, що загальними типами arr є String. Це тонка різниця, але це важливо. Оскільки arr створюється під час виконання, загальні типи стираються під час виконання, тому ви не можете зрозуміти це. Як заявлено деякі, ArrayList<Integer>виглядає так само, як і ArrayList<String>з точки зору відображення.

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

Factory<MySpecialObject> factory = new Factory<MySpecialObject>();
MySpecialObject obj = factory.create();

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

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


1

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

Велике психологічне роздратування: я час від часу потрапляю в ситуації, коли генерики не можуть працювати. (Масиви є найпростішим прикладом.) І я не можу зрозуміти, чи може дженерік зробити цю роботу чи я просто дурний. Ненавиджу це. Щось на зразок дженерики повинно працювати завжди. Кожен раз, коли я не можу робити те, що хочу, використовуючи мову на мові Java, я знаю, що проблема - це я, і я знаю, якщо я просто продовжую натискати, я зрештою доїду. З дженериками, якщо я стану надто наполегливим, я можу витрачати багато часу.

Але справжня проблема полягає в тому, що дженерики додають занадто багато складності для занадто малого виграшу. У найпростіших випадках це може перешкодити мені додавати яблуко до списку, що містить машини. Чудово. Але без дженериків ця помилка могла б дуже швидко запустити ClassCastException під час роботи з невеликим витраченим часом. Якщо я додам автомобіль з дитячим сидінням і дитиною в ньому, чи потрібно мені попереджувати час складання, щоб перелік був лише для автомобілів з дитячими сидіннями, де містяться дитячі шимпанзе? Список простих примірників об’єкта починає виглядати як гарна ідея.

Код Generic'ed може містити багато слів і символів, займає багато зайвого місця та займає читання набагато довше. Я можу витратити багато часу на те, щоб весь цей додатковий код працював правильно. Коли це відбувається, я відчуваю себе безумно розумним. Я також витрачав кілька годин або більше свого власного часу, і мені потрібно задуматися, чи зможе хто-небудь ще розібратися з кодом. Я хотів би мати можливість його віддати на технічне обслуговування людям, які менш розумні, ніж я, або які мають менше часу витрачати.

З іншого боку (я трохи закрутився там і відчуваю потребу в забезпеченні балансу), приємно, коли у використанні простих колекцій і карт є додавання, додавання та перевірка під час написання, і зазвичай це не додає багато складність коду ( якщо хтось інший пише колекцію чи карту) . І Java в цьому краще, ніж C #. Здається, жодна з колекцій C #, яку я хочу використати, не обробляє дженерики. (Зізнаюся, у мене в колекціях дивні смаки.)


Приємна черга. Однак існує багато бібліотек / рамок, які можуть існувати без дженерики, наприклад, Guice, Gson, Hibernate. І генерики не такі складні, як тільки ти звикнеш до цього. Введення та читання аргументів типу - це справжня PITA; тут val допомагає, якщо ви можете використовувати.
maaartinus

1
@maaartinus: Написав це деякий час тому. Також ОП запитала "питання". Я вважаю, що дженерики є надзвичайно корисними і дуже подобаються їм - набагато більше, ніж тоді. Однак, якщо ви пишете власну колекцію, їх дуже важко вивчити. І їх корисність згасає, коли тип колекції визначається під час виконання - коли у колекції є метод, який повідомляє вам клас її записів. На даний момент дженерики не працюють, і ваш IDE видасть сотні безглуздих попереджень. 99,99% програмування Java цього не передбачає. Але я просто мав багато проблем з цим, коли писав вище.
РальфЧапін

Будучи розробником Java та C #, мені цікаво, чому ви віддасте перевагу колекціям Java?
Івайло Славов

Fine. But without generics this error would throw a ClassCastException really quick at run time with little time wasted.Це дійсно залежить від того, скільки часу запускається між запуском програми та потраплянням у відповідний рядок коду. Якщо користувач повідомляє про помилку, і вам знадобиться кілька хвилин (або, в найгіршому випадку, години або навіть дні) для відтворення, перевірка часу компіляції починає виглядати все краще і краще ...
Мейсон Уілер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.