Чому я повинен дбати про те, щоб у Java не було оновлених дженериків?


102

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

Очевидно, через те, що необмежені типи дозволені в Java (і небезпечні перевірки), можна підривати генеричні дані і в кінцевому підсумку мати List<Integer>те, що (наприклад) насправді містить Strings. Це, очевидно, могло б бути неможливим, якщо інформація про тип був перероблений; але повинно бути більше, ніж це !

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

public <T> void foo(List<T> l) {
   if (l.getGenericType() == Integer.class) {
       //yeah baby! err, what now?

EDIT : Швидке оновлення цього питання, оскільки відповіді, як видається, в основному викликає занепокоєння щодо необхідності передачі в Classякості параметра (наприклад EnumSet.noneOf(TimeUnit.class)). Я шукав більше чогось там, де це просто неможливо . Наприклад:

List<?> l1 = api.gimmeAList();
List<?> l2 = api.gimmeAnotherList();

if (l1.getGenericType().isAssignableFrom(l2.getGenericType())) {
    l1.addAll(l2); //why on earth would I be doing this anyway?

Це означає, що ви можете отримати клас загального типу під час виконання? (якщо так, у мене є приклад!)
Джеймс Б

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

2
Більш цікаве питання (для мене): що знадобиться для реалізації загальної стилістики стилю C ++ на Java? Це, звичайно, здається можливим під час виконання, але зламає всі існуючі завантажувачі класів (тому findClass()що доведеться ігнорувати параметризацію, але defineClass()не зміг). І як ми знаємо, сили, які мають утримувати зворотну сумісність, першорядні.
kdgregory

1
Насправді, Java надає реіфіковані дженерики дуже обмеженим чином . Я надаю більше деталей у цій темі SO: stackoverflow.com/questions/879855/…
Річард Гомес

Основна інформація JavaOne вказує на те, що Java 9 підтримуватиме вдосконалення.
Рангі Кін

Відповіді:


81

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

public class Foo<T> {

    private T t;

    public Foo() {
        this.t = new T(); // Help?
    }

}

Це працює в C #, якщо Tмати конструктор за замовчуванням . Ви навіть можете отримати тип виконання typeof(T)та отримати конструктори Type.GetConstructor().

Загальним рішенням Java було б передавати Class<T>як аргумент.

public class Foo<T> {

    private T t;

    public Foo(Class<T> cls) throws Exception {
        this.t = cls.newInstance();
    }

}

(це не обов'язково потрібно передавати як аргумент конструктора, оскільки аргумент методу також чудовий, наведене вище - лише приклад, також try-catchкороткості опущено)

Для всіх інших конструкцій загального типу фактичний тип легко можна вирішити за допомогою трохи рефлексії. Нижче наведені запитання ілюструють випадки використання та можливості використання:


5
Це працює (наприклад) у C #? Звідки ви знаєте, що T має конструктор за замовчуванням?
Томас Юнг

4
Так, це роблять. І це лише базовий приклад (припустимо, що ми працюємо з javabeans). Вся справа в тому, що з Java ви не можете отримати клас під час виконання через T.classабо T.getClass(), щоб ви мали доступ до всіх його полів, конструкторів та методів. Це робить будівництво також неможливим.
BalusC

3
Це здається мені дуже слабким як "велика проблема", тим більше, що це може бути корисно лише в поєднанні з дуже крихким відображенням навколо конструкторів / параметрів тощо.
oxbow_lakes

33
Вона компілюється в C # за умови, що ви оголошуєте тип як: public class Foo <T> де T: new (). Що обмежить дійсні типи T лише тими, що містять конструктор без параметрів.
Мартін Харріс

3
@Colin Ви фактично можете сказати Java, що для розширення більш ніж одного класу чи інтерфейсу потрібно певний загальний тип. Дивіться розділ "Кілька рядків " на docs.oracle.com/javase/tutorial/java/generics/bounded.html . (Звичайно, ви не можете сказати, що об'єкт буде одним із певних наборів, які не мають спільних методів, які можуть бути абстраговані в інтерфейсі, які можуть бути дещо реалізовані в C ++ за допомогою спеціалізації шаблону.)
JAB

99

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

public void my_method(List<String> input) { ... }
public void my_method(List<Integer> input) { ... }

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

26
@Tom: це навіть не компілюється через стирання типу. Обидва складаються як public void my_method(List input) {}. Однак я ніколи не стикався з цією потребою, просто тому, що вони не мали б того самого імені. Якщо вони мають одне ім’я, я б запитав, чи public <T extends Object> void my_method(List<T> input) {}не краща ідея.
BalusC

1
Гм, я б схильний взагалі уникати перевантаження з однаковою кількістю параметрів, і волію щось подібне myStringsMethod(List<String> input)і myIntegersMethod(List<Integer> input)навіть якщо перевантаження для такого випадку було можливим на Java.
Fabian Steeg

4
@Fabian: Це означає, що ви повинні мати окремий код і запобігати тим, які переваги ви отримуєте від <algorithm>C ++.
Девід Торнлі

Я розгублений, це по суті те саме, що і мій другий пункт, але все-таки я отримав 2 посилання на це? Хтось дбає просвітити мене?
rsp

34

Безпека типу приходить на думку. Перехід на параметризований тип завжди буде небезпечним без перероблених дженериків:

List<String> myFriends = new ArrayList();
myFriends.add("Alice");
getSession().put("friends", myFriends);
// later, elsewhere
List<Friend> myFriends = (List<Friend>) getSession().get("friends");
myFriends.add(new Friend("Bob")); // works like a charm!
// and so...
List<String> myFriends = (List<String>) getSession().get("friends");
for (String friend : myFriends) print(friend); // ClassCastException, wtf!? 

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


1
Так - у мене є спосіб подолати це тим, що я створю ParametrizedListкопію даних, що копіюють дані в типи перевірки колекції джерел. Це трохи схоже, Collections.checkedListале можна посіяти колекцією для початку.
oxbow_lakes

@tackline - ну, кілька абстракцій витікатимуть менше. Якщо вам потрібен доступ до введення метаданих у вашій реалізації, зовнішній інтерфейс повідомить про вас, оскільки клієнтам потрібно надіслати вам об’єкт класу.
gustafc

... це означає, що за допомогою реінфікованих дженериків ви можете додавати такі речі, як T.class.getAnnotation(MyAnnotation.class)(де Tє загальний тип), не змінюючи зовнішній інтерфейс.
gustafc

@gustafc: якщо ви думаєте, що шаблони C ++ забезпечують повну безпеку типу, прочитайте це: kdgregory.com/index.php?page=java.generics.cpp
kdgregory

@kdgregory: Я ніколи не говорив, що C ++ на 100% безпечний - саме це стирання шкодить безпеці типу. Як ви самі кажете, "C ++, виявляється, має власну форму стирання типу, відому як анонсований покажчик у стилі С". Але Java виконує лише динамічні касти (не переінтерпретовані), тому реіфікація включить це все в систему типів.
gustafc

26

Ви зможете створити загальні масиви у своєму коді.

public <T> static void DoStuff() {
    T[] myArray = new T[42]; // No can do
}

що не так з Об'єктом? Масив об’єктів - це масив посилань у будь-якому випадку. Це не так, як об'єкти дані сидять на стеці - все це в купі.
Ран Бірон

19
Тип безпеки. Я можу помістити все, що хочу, в Object [], але тільки Strings в String [].
Тернор

3
Ран: Без саркастичності: Можливо, ви хочете використовувати мову сценаріїв замість Java, тоді у вас є гнучкість нетипізованих змінних де завгодно!
літаючі вівці

2
Масиви є коваріантними (і, отже, не безпечними) для обох мов. String[] strings = new String[1]; Object[] objects = strings; objects[0] = new Object();Компілюється чудово на обох мовах. Не працює тонко.
Мартійн

16

Це давнє запитання, є багато відповідей, але я думаю, що відповіді вже не знаходяться.

"reified" просто означає реальне і зазвичай просто означає протилежне стирання типу.

Велика проблема, пов’язана з Java Generics:

  • Ця жахлива вимога до боксу та відключення між примітивами та референтними типами. Це не пов’язано безпосередньо з переробкою чи стиранням типу. C # / Scala виправити це.
  • Немає власних типів. З цієї причини JavaFX 8 довелося видалити "будівельників". Абсолютно нічого спільного з стиранням типу. Скала це виправляє, не впевнений у C #.
  • Немає дисперсії на стороні декларації. C # 4.0 / Scala мають це. Абсолютно нічого спільного з стиранням типу.
  • Неможливо перевантажити void method(List<A> l)і method(List<B> l). Це відбувається через стирання типу, але надзвичайно дрібне.
  • Немає підтримки для відображення типу виконання. Це серце стирання типу. Якщо вам подобаються супер просунуті компілятори, які перевіряють і доводять більшу частину логіки вашої програми під час компіляції, вам слід використовувати рефлексію якомога менше, і такий тип стирання типу не повинен вас турбувати. Якщо вам більше подобається нескінченне, сценарійне, динамічне програмування типу, і ви не так переймаєтесь компілятором, який підтверджує як можна більшу частину вашої логіки, тоді ви бажаєте кращого відображення та виправлення стирання типу.

1
Здебільшого мені важко із випадками серіалізації. Ви часто хотіли б мати можливість обнюхувати типи класів загальних речей, отримуючи серіалізацію, але вас не зупиняють через стирання типу. Зробити щось подібне важкоdeserialize(thingy, List<Integer>.class)
Cogman

3
Я думаю, що це найкраща відповідь. Особливо частин, що описують, які проблеми справді є фундаментальними через стирання типу, а які лише проблеми дизайну мови Java. Перше, що я кажу людям, які починають стирання стирань, - це те, що Скала та Хаскелл теж працюють за типом стирання.
aemxdp

13

Серіалізація була б простішою з переробкою. Чого б ми хотіли, так і є

deserialize(thingy, List<Integer>.class);

Що ми маємо зробити - це

deserialize(thing, new TypeReference<List<Integer>>(){});

виглядає некрасиво і працює чудово.

Також є випадки, коли було б дуже корисно сказати щось подібне

public <T> void doThings(List<T> thingy) {
    if (T instanceof Q)
      doCrazyness();
  }

Ці речі не кусаються часто, але кусаються, коли вони трапляються.


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

11

Моє вплив на Java Geneircs є досить обмеженим, окрім пунктів, про які вже згадували інші відповіді, є сценарій, пояснений у книзі Java Generics and Collections , Моріса Нафталіна та Філіпа Уолдера, де корисні редактичні дженерики корисні.

Оскільки типи не підлягають повторенню, не можна мати параметризовані винятки.

Наприклад, декларація нижче наведеної форми не є дійсною.

class ParametericException<T> extends Exception // compile error

Це тому, що улов пункті перевіряється, чи відповідає викинутий виняток даному типу. Ця перевірка є такою ж, як перевірка, виконана тестом екземпляра, і оскільки тип не може повторно повторюватися, наведена вище форма оператора недійсна.

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

try {
     throw new ParametericException<Integer>(42);
} catch (ParametericException<Integer> e) { // compile error
  ...
}

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


1
Це дійсна точка, але я не зовсім впевнений, чому такий параметризований клас виключень був би корисним. Чи можете ви змінити свою відповідь, щоб вона містила короткий приклад, коли це може бути корисним?
oxbow_lakes

@oxbow_lakes: Вибачте Мої знання про Java Generics досить обмежені, і я намагаюся вдосконалити її. Тож зараз я не в змозі придумати жодного прикладу, де параметризований виняток може бути корисним. Спробую подумати над цим. Дякую.
sateesh

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

Поліпшення продуктивності полягає в тому, що в даний час параметр типу повинен успадковуватися від Object, вимагаючи боксу примітивних типів, що накладає накладні виконання та пам'ять.
meriton

8

Масиви, мабуть, гратимуть набагато приємніше з генериками, якби їх реіфікували.


2
Звичайно, але у них все одно будуть проблеми. List<String>не є List<Object>.
Том Хотін - лінійка

Погодьтеся - але лише для примітивів (Integer, Long та ін.). Для "звичайного" Об'єкта це те саме. Оскільки примітиви не можуть бути параметризованим типом (набагато серйозніша проблема, принаймні ІМХО), я не вважаю це справжнім болем.
Ран Бірон

3
Проблема з масивами полягає в їхній коваріації, нічого спільного з реіфікацією.
Повторити

5

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

API виглядає так Iterator<T> T - якийсь тип, який можна побудувати, використовуючи лише рядки в конструкторі. Потім ітератор переглядає, що рядки повертаються з запиту sql, а потім намагаються зіставити його з конструктором типу T.

У поточному способі, що реалізується в загальній формі, я також повинен передавати в клас об'єктів, які я буду створювати з мого набору результатів. Якщо я правильно розумію, якби дженерики були реіфіковані, я міг би просто зателефонувати T.getClass () отримати його конструктори, а потім не доведеться відкидати результат Class.newInstance (), який був би далеко охайнішим.

В основному, я думаю, що це спрощує написання API (на відміну від просто написання програми), тому що ви можете зробити набагато більше об'єктів, і, таким чином, буде потрібно менше конфігурації ... Я не оцінив наслідки анотацій, поки я не бачив, як вони використовуються в таких речах, як spring або xstream замість патрубок config.


Але проходження в клас мені здається безпечнішим в усьому напрямку. У будь-якому випадку, рефлекторне створення екземплярів із запитів до бази даних надзвичайно крихке до таких змін, як рефакторинг у будь-якому випадку (і у вашому коді, і в базі даних). Напевно, я шукав речі, за якими просто неможливо надати клас
oxbow_lakes

5

Одна приємна річ - уникати боксу для примітивних (ціннісних) типів. Це дещо пов’язане зі скаргою на масив, яку викликали інші, і у випадках, коли використання пам’яті обмежено, це фактично може суттєво змінитись.

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


2
Це по суті зводиться до того, що можна робити new T[]там, де T первісного типу!
oxbow_lakes

2

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

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


Це, звичайно, те, що дженерики є в Java . У C # та інших мовах вони є потужним інструментом
Basic

1

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

    public interface Service<KEY,VALUE> {
           VALUE get(KEY key);
    }

    public class PersonService implements Service<Long, Person>,
        Service<String, Person> //Can not do!!

0

Ось один, який мене сьогодні зачепив: без внесення змін, якщо ви пишете метод, який приймає список варагрів із загальних предметів ... абоненти можуть думати, що вони безпечні, але випадково переходять у будь-яку стару грубість і підірвуть ваш метод.

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

(NB: Я, можливо, тут помилився, але гугляючи навколо "generics varargs", вище, здається, саме те, що ви очікували. Те, що робить це практичною проблемою, - це використання класу, я думаю - абонентів здається бути менш обережним :()


Наприклад, я використовую парадигму, яка використовує об’єкти Class як ключ у картах (це складніше, ніж проста карта - але концептуально це відбувається).

наприклад, це чудово працює в Java Generics (тривіальний приклад):

public <T extends Component> Set<UUID> getEntitiesPossessingComponent( Class<T> componentType)
    {
        // find the entities that are mapped (somehow) from that class. Very type-safe
    }

наприклад, без переробки в Java Generics, цей приймає будь-який об’єкт "Class". І це лише невелике розширення попереднього коду:

public <T extends Component> Set<UUID> getEntitiesPossessingComponents( Class<T>... componentType )
    {
        // find the entities that are mapped (somehow) to ALL of those classes
    }

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

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