Чому не повинні літери Java enum мати загальні параметри типу?


148

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

public enum MyEnum<T> {
    LITERAL1<String>,
    LITERAL2<Integer>,
    LITERAL3<Object>;
}

Цей параметр загального типу <T>в свою чергу може бути корисним у різних місцях. Уявіть параметр загального типу для методу:

public <T> T getValue(MyEnum<T> param);

Або навіть у самому класі enum:

public T convert(Object o);

Більш конкретний приклад №1

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

  • Перелічує, бо тоді я можу перерахувати скінченний набір властивостей ключів
  • Загальне, тому що тоді я можу забезпечити безпеку типу типу для зберігання властивостей
public interface MyProperties {
     public <T> void put(MyEnum<T> key, T value);
     public <T> T get(MyEnum<T> key);
}

Більш конкретний приклад №2

У мене перерахування типів даних:

public interface DataType<T> {}

public enum SQLDataType<T> implements DataType<T> {
    TINYINT<Byte>,
    SMALLINT<Short>,
    INT<Integer>,
    BIGINT<Long>,
    CLOB<String>,
    VARCHAR<String>,
    ...
}

Кожен переслідник перерахунку очевидно мав би додаткові властивості, засновані на родовому типі <T>, в той же час, будучи перерахунком (незмінним, синглетним, перелічуючим тощо) тощо.

Питання:

Ніхто про це не думав? Це обмеження щодо компілятора? Враховуючи той факт, що ключове слово " enum " реалізовано як синтаксичний цукор, що представляє згенерований код JVM, я не розумію цього обмеження.

Хто може мені це пояснити? Перш ніж відповісти, подумайте про це:

  • Я знаю, що родові типи стираються :-)
  • Я знаю, що існують обхідні шляхи використання об’єктів Class. Вони обхідні шляхи.
  • Загальні типи призводять до того, що вони генеруються компілятором, коли вони застосовуються (наприклад, при виклику методу convert ()
  • Родовий тип <T> був би на перерахунку. Отже, він пов'язаний кожним із буквалів перерахунку. Отже, компілятор буде знати, який тип застосувати, коли пише щось подібнеString string = LITERAL1.convert(myObject); Integer integer = LITERAL2.convert(myObject);
  • Це ж стосується параметра загального типу в T getvalue()методі. Компілятор може застосовувати кастинг типів під час викликуString string = someClass.getValue(LITERAL1)

3
Я також не розумію цього обмеження. Нещодавно я натрапив на це, коли мій перелік містив різні типи "Порівнювані", а з генеричними засобами можна порівняти лише порівнянні типи одних і тих же типів без попереджень, які потрібно придушити (навіть якщо під час виконання відповідні типи можна було б порівняти). Я міг би позбутися цих попереджень, використовуючи тип, пов'язаний в enum, щоб вказати, який порівняний тип підтримується, але замість цього мені довелося додати примітку SuppressWarnings - ніякого способу цього не обійти! Оскільки
CompareTo

5
(+1) Я якраз посеред спроби закрити розрив безпеки в моєму проекті, зупинений мертвим цим довільним обмеженням. Просто врахуйте це: перетворіть на enumідіому "typesafe enum", яку ми використовували до Java 1.5. Раптом у вас можуть бути налаштовані параметри членів перерахунку. Це, мабуть, те, що я зараз буду робити.
Марко Тополник

1
@EdwinDalorzo: оновив питання конкретним прикладом від jOOQ , де це було б неабияк корисним у минулому.
Лукас Едер

2
@LukasEder Я зараз бачу вашу думку. Нова функція виглядає круто. Можливо, вам слід запропонувати це у списку розсилки проектних монет. Я бачу інші цікаві пропозиції на переліках, але не такі, як ваша.
Едвін Далорцо

1
Повністю згоден. Енум без дженериків покалічений. Ваш випадок №1 також мій. Якщо мені потрібен загальний перелік, я відмовляюся від JDK5 та впроваджую його у звичайному старому стилі Java 1.4. Цей підхід має і додаткову користь : я не змушений мати всі константи в одному класі або навіть пакеті. Таким чином, стиль упаковки за функцією виконується набагато краще. Це ідеально підходить для конфігураційних "переліків" - константи розбиваються по пакетах відповідно до їх логічного значення (і якщо я хочу бачити їх усі, у мене показана ієрархія типу).
Томаш Залуський

Відповіді:


50

Зараз це обговорюється з розширеними перерахунками JEP-301 . Приклад, наведений в JEP, це саме те, що я шукав:

enum Argument<X> { // declares generic enum
   STRING<String>(String.class), 
   INTEGER<Integer>(Integer.class), ... ;

   Class<X> clazz;

   Argument(Class<X> clazz) { this.clazz = clazz; }

   Class<X> getClazz() { return clazz; }
}

Class<String> cs = Argument.STRING.getClazz(); //uses sharper typing of enum constant

На жаль, JEP все ще бореться зі значними проблемами: http://mail.openjdk.java.net/pipermail/amber-spec-experts/2017-May/000041.html


2
Станом на грудень 2018 року навколо JEP 301 знову з’явилися певні ознаки життя , але, проігнорувавши цю дискусію, зрозуміло, що проблеми ще далеко не вирішені.
Понт

11

Відповідь у запитанні:

через стирання типу

Жоден із цих двох методів неможливий, оскільки тип аргументу стирається.

public <T> T getValue(MyEnum<T> param);
public T convert(Object);

Для реалізації цих методів ви могли б побудувати перерахунок як:

public enum MyEnum {
    LITERAL1(String.class),
    LITERAL2(Integer.class),
    LITERAL3(Object.class);

    private Class<?> clazz;

    private MyEnum(Class<?> clazz) {
      this.clazz = clazz;
    }

    ...

}

2
Що ж, стирання відбувається під час компіляції. Але компілятор може використовувати загальну інформацію про тип для перевірки типу. А потім "перетворює" загальний тип на типові касти. Я перефразую питання
Лукаш Едер

2
Не впевнений, що я цілком слідую. Беріть public T convert(Object);. Я здогадуюсь, що цей метод може, наприклад, звузити купу різних типів до <T>, що, наприклад, є String. Побудова об'єкта String - це час виконання - нічого, що компілятор не робить. Отже, вам потрібно знати тип виконання, наприклад String.class. Або я щось пропускаю?
Мартін Альгестен

6
Я думаю, вам не вистачає факту, що загальний тип <T> знаходиться на enum (або його створеному класі). Єдиними екземплярами перерахунку є його літерали, які забезпечують постійне родове зв'язування типу. Отже, немає ніякої неоднозначності щодо T у загальнодоступному перетворенні T (Об'єкт);
Лукас Едер

2
Ага! Зрозумів. Ти розумніший за мене :)
Мартін Алгестен

1
Я думаю, що це зводиться до класу для того, щоб перерахунок оцінювався аналогічно до всього іншого. У Java, хоча речі здаються постійними, їх немає. Розглянемо public final static String FOO;статичний блок static { FOO = "bar"; }- також константи оцінюються, навіть коли вони відомі під час компіляції.
Мартін Альгестен

5

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

Оновлення: наразі додається до мови під JEP 301: Enhanced Enums .


Не могли б ви детальніше зупинитися на ускладненнях?
Mr_and_Mrs_D

4
Я про це думаю вже пару днів, і досі не бачу ніяких ускладнень.
Марко Тополник

4

Є інші методи в ENUM, які не працюють. Що б MyEnum.values()повернути?

Про що MyEnum.valueOf(String name)?

Для valueOf, якщо ви думаєте, що компілятор міг зробити загальний метод на зразок

загальнодоступна статична MyEnum valueOf (назва рядка);

для того, щоб назвати це так MyEnum<String> myStringEnum = MyEnum.value("some string property"), це теж не вийшло. Наприклад, що робити, якщо ви телефонуєте MyEnum<Int> myIntEnum = MyEnum.<Int>value("some string property")? Неможливо реалізувати цей метод, щоб він працював правильно, наприклад, щоб викинути виняток або повернути null, коли ви викликаєте його, як MyEnum.<Int>value("some double property")через стирання типу.


2
Чому б вони не працювали? Вони просто використовуватимуть підстановку…: MyEnum<?>[] values()таMyEnum<?> valueOf(...)
Лукас Едер

1
Але тоді ви не могли виконати таке завдання, MyEnum<Int> blabla = valueOf("some double property");оскільки типи не сумісні. Крім того, ви хочете, щоб у цьому випадку отримати нуль, тому що ви хочете повернути MyEnum <Int>, який не існує для подвійного імені властивості, і ви не можете змусити цей метод працювати належним чином через стирання.
користувач1944408

Крім того, якщо ви перебираєте значення (), вам потрібно буде використовувати MyEnum <?>, Що зазвичай не є тим, що ви хочете, тому що, наприклад, ви не можете перебирати лише свої Int властивості. Також вам потрібно зробити багато кастингу, якого ви хочете уникати, я б запропонував створити різні переліки для кожного типу або створити свій власний клас із примірниками ...
user1944408

Ну, я думаю, ви не можете мати обох. Зазвичай я вдаюсь до власних реалізацій "enum". Просто Enumє багато інших корисних функцій, і ця була б необов’язковою і дуже корисною також ...
Лукаш Едер,

0

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

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

Візьмемо приклад перерахунку з підручника. Це не дуже корисно чи послідовно:

public enum Planet<T>{
    Earth<Planet>,
    Venus<String>,
    Mars<Long>
    ...etc.
}

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

Крім того, як би ви управляли складними перетвореннями?

наприклад

public enum BadIdea<T>{
   INSTANCE1<Long>,
   INSTANCE2<MyComplexClass>;
}

Його досить просто, щоб String Integerввести ім'я або порядковий номер. Але дженерики дозволять вам поставити будь-який тип. Як би ви керували конверсією MyComplexClass? Тепер ваше гнітюче два конструкти, змушуючи компілятор знати, що існує обмежена підмножина типів, які можна поставити до загальних переліків, і вводити додаткову плутанину в концепцію (Generics), яка, здається, ухиляється від багатьох програмістів.


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

1
Приклади підтверджують суть. Екземпляри перерахунків - це підкласи типу (приємно і просто), і те, що включає генерику, - це глисти, це складність для дуже незрозумілої користі. Якщо ви збираєтесь зневажати мене, то вам також слід знищити Тома Хоутіна нижче, який сказав те саме в не так багато слів
nsfyn55

1
@ nsfyn55 Enums - одна з найскладніших, наймагічніших мовних особливостей Java. Немає жодної іншої функції мови, яка, наприклад, автоматично генерує статичні методи. Крім того, кожен тип enum вже є екземпляром родового типу.
Марко Топольник

1
@ nsfyn55 Метою дизайну було зробити членів enum потужними та гнучкими, тому вони підтримують такі розширені функції, як власні методи екземпляра та навіть змінні екземпляри змінних. Вони розроблені таким чином, щоб поведінка спеціалізувалася для кожного члена окремо, щоб вони могли брати участь як активні співробітники у складних сценаріях використання. Більш за все, перелік Java був розроблений для того, щоб зробити загальний тип ідіоми перерахування безпечним , але відсутність параметрів типу, на жаль, не відповідає цій найзаповітнішій цілі. Я цілком переконаний, що для цього була дуже конкретна причина.
Марко Тополник

1
Я не помітив вашої згадки про протиріччя ... Java це підтримує, тільки це веб-сайт використання, що робить його трохи непростим. interface Converter<IN, OUT> { OUT convert(IN in); } <E> Set<E> convertListToSet(List<E> in, Converter<? super List<E>, ? extends Set<E>> converter) { return converter.convert(in); }Нам потрібно вручну опрацьовувати кожен раз, який тип споживається та який виробляється, і визначати межі відповідно.
Марко Тополник

-2

Becasue "enum" - це абревіатура для перерахування. Це просто набір іменованих констант, які стоять замість порядкових чисел, щоб зробити код легше читабельним.

Я не бачу, яким міг бути призначений зміст константної параметри типу.


1
В java.lang.String: public static final Comparator<String> CASE_INSENSITIVE_ORDER. Ви бачите зараз? :-)
Лукас Едер

4
Ви сказали, що не бачите, яке значення може мати константа, параметризована типу. Тому я показав вам константну параметризацію типу (не функцію вказівника).
Лукас Едер

Звичайно, ні, оскільки мова Java не знає такого поняття, як покажчик функції. Все-таки не константа параметризована типу, але це тип. І сам параметр типу є постійним, а не змінною типу.
Інго

Але синтаксис enum - це просто синтаксичний цукор. Внизу вони точно схожі на CASE_INSENSITIVE_ORDER... Якби перерахунок Comparator<T>був, чому б не мати літералів з будь-якими пов'язаними пов'язаними <T>?
Лукас Едер

Якби компаратор <T> був перерахунком, то справді ми могли б мати усілякі дивні речі. Можливо, щось на зразок Integer <T>. Але це не так.
Інго

-3

Я думаю, тому що в основному Enums неможливо застосувати

Де б ви встановили клас T, якщо JVM дозволив вам це зробити?

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

новий MyEnum <> ()?

Однак наступний підхід може бути корисним

public enum MyEnum{

    LITERAL1("s"),
    LITERAL2("a"),
    LITERAL3(2);

    private Object o;

    private MyEnum(Object o) {
        this.o = o;
    }

    public Object getO() {
        return o;
    }

    public void setO(Object o) {
        this.o = o;
    }   
}

8
Не впевнений, що мені подобається setO()метод на enum. Я вважаю константи перерахунків, і для мене це означає незмінне . Тож навіть якщо це можливо, я б цього не робив.
Мартін Альгестен

2
Енуми інстанціюються в коді, згенерованому компілятором. Кожен буквал насправді створюється за допомогою виклику приватних конструкторів. Отже, є можливість передавати загальний тип конструктору під час компіляції.
Лукас Едер

2
Населення на перерахунки - це абсолютно нормально. Особливо, якщо ви використовуєте enum для створення сингтона (Пункт 3 Ефективна Java Джошуа Блоха)
Престон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.