Почему литералы перечисления Java не должны иметь параметров общего типа?

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, я не понимаю этого ограничения.

Кто может объяснить это мне? Прежде чем ответить, подумайте об этом:

  • Я знаю, что общие типы стираются :-)
  • Я знаю, что есть обходные пути с использованием объектов класса. Они обходные пути.
  • Универсальные типы приводят к генерации типов, сгенерированной компилятором, где это применимо (например, при вызове метода convert ()
  • Универсальный тип <T> будет в перечислении. Следовательно, он связан каждым из литералов перечисления. Следовательно, компилятор будет знать, какой тип применять при написании чего-то вродеString string = LITERAL1.convert(myObject); Integer integer = LITERAL2.convert(myObject);
  • То же самое относится к параметру универсального типа в T getvalue()методе. Компилятор может применять приведение типов при вызовеString string = someClass.getValue(LITERAL1)
Лукас Эдер
источник
3
Я тоже не понимаю этого ограничения. Я недавно сталкивался с этим, когда мое перечисление содержало различные типы «Comparable», а с универсальными типами можно сравнивать только сопоставимые типы тех же типов без предупреждений, которые необходимо подавлять (даже если во время выполнения будут сравниваться правильные типы). Я мог бы избавиться от этих предупреждений, используя тип, связанный в перечислении, чтобы указать, какой сопоставимый тип поддерживается, но вместо этого мне пришлось добавить аннотацию SuppressWarnings - никак не обойтись! Так как compareTo в любом случае
выдает
5
(+1) Я как раз пытаюсь закрыть пробел в типе в моем проекте, остановленный мертвым из-за этого произвольного ограничения. Просто подумайте об этом: превратите enumв идиому «типизированное перечисление», который мы использовали до Java 1.5. Внезапно вы можете настроить параметры перечисления. Это, вероятно, то, что я собираюсь сделать сейчас.
Марко Топольник
1
@EdwinDalorzo: Обновил вопрос с конкретным примером из jOOQ , где это было бы очень полезно в прошлом.
Лукас Эдер
2
@LukasEder Теперь я понимаю вашу точку зрения. Похоже, крутая новая функция. Возможно, вам стоит предложить это в списке рассылки проекта. Я вижу там другие интересные предложения по перечислениям, но не такие, как у вас.
Эдвин Далорсо
1
Полностью согласен. Enum без генериков покалечен. Ваш случай № 1 также мой. Если мне нужен универсальный enum, я отказываюсь от 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);

Однако, чтобы реализовать эти методы, вы можете построить свой enum следующим образом:

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 convert (Object);
Лукас Эдер
2
Ага! Понял. Ты умнее меня :)
Мартин Альгестен
1
Я предполагаю, что это сводится к классу для того, чтобы перечисление оценивалось аналогично всему остальному. В Java, хотя вещи кажутся постоянными, это не так. Рассмотрим public final static String FOO;статический блок static { FOO = "bar"; }- константы также оцениваются, даже если они известны во время компиляции.
Мартин Альгестен
5

Потому что ты не можешь. Шутки в сторону. Это можно добавить к спецификации языка. Это не было Это добавило бы некоторую сложность. Эта выгода от стоимости означает, что она не является приоритетной.

Обновление: в настоящее время добавляется к языку в JEP 301: Enhanced Enums .

Том Хотин - Tackline
источник
Не могли бы вы рассказать о осложнениях?
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")? Невозможно реализовать этот метод для правильной работы, например, чтобы выдать исключение или вернуть ноль, когда вы вызываете его как, MyEnum.<Int>value("some double property")из-за стирания типа.

user1944408
источник
2
Почему они не работают? Они просто использовали бы групповые символы ...: MyEnum<?>[] values()иMyEnum<?> valueOf(...)
Лукас Эдер
1
Но тогда вы не могли бы делать присваивания, как это, MyEnum<Int> blabla = valueOf("some double property");потому что типы не совместимы. Также в этом случае вы хотите получить значение null, потому что вы хотите вернуть MyEnum <Int>, который не существует для двойного имени свойства, и вы не можете заставить этот метод работать должным образом из-за удаления.
user1944408
Также, если вы перебираете значения (), вам нужно будет использовать MyEnum <?>, Что обычно не то, что вам нужно, потому что, например, вы не можете перебирать только свои свойства Int. Также вам нужно будет выполнить много приведений, которых вы хотите избежать, я бы предложил создать разные перечисления для каждого типа или создать свой собственный класс с экземплярами ...
user1944408
Ну, я думаю, вы не можете иметь оба. Обычно я прибегаю к своим собственным реализациям enum. Просто у Enumнего много других полезных функций, и эта будет необязательной и очень полезной ...
Лукас Эдер
0

Откровенно говоря, это больше похоже на решение проблемы, чем что-либо еще.

Вся цель перечисления java состоит в том, чтобы смоделировать перечисление экземпляров типов, которые имеют общие свойства таким образом, что обеспечивает согласованность и богатство, превосходящие сопоставимые представления String или Integer.

Возьмите пример перечисления учебника. Это не очень полезно или не соответствует:

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

Зачем мне хотеть, чтобы на разных планетах были разные преобразования типов? Какую проблему это решает? Оправдывает ли это усложнение языковой семантики? Если мне нужно такое поведение, является ли enum лучшим инструментом для достижения этого?

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

например

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

Достаточно просто String Integerуказать имя или порядковый номер. Но дженерики позволят вам поставить любой тип. Как бы вы справились с преобразованием в MyComplexClass? Теперь вы смешиваете две конструкции, заставляя компилятор знать, что существует ограниченное подмножество типов, которые могут быть предоставлены универсальным перечислениям, и вводите дополнительную путаницу в концепцию (Generics), которая, похоже, уже ускользает от многих программистов.

nsfyn55
источник
17
Думая о нескольких примерах, где это было бы бесполезно, это ужасный аргумент, что он никогда не будет полезен.
Элиас Василенко
1
Примеры подтверждают это. Экземпляры enum являются подклассами типа (приятного и простого), и включение в него дженериков может привести к усложнению с точки зрения сложности. Если вы собираетесь понизить меня, то вам также следует понизить голос Тома Хотина ниже, который сказал то же самое не так много слов
nsfyn55
1
@ nsfyn55 Перечисления - одна из самых сложных и волшебных функций языка Java. Например, нет ни одной другой языковой функции, которая автоматически генерирует статические методы. Кроме того, каждый тип перечисления уже является экземпляром универсального типа.
Марко Топольник
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>?
Лукас Эдер
Если бы Comparator <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;
    }   
}
CAPSULA
источник
8
Не уверен, что мне нравится setO()метод на enum. Я считаю перечисления константами и для меня это подразумевает неизменность . Поэтому, даже если это возможно, я бы не стал этого делать.
Мартин Альгестен
2
Перечисления создаются в коде, сгенерированном компилятором. Каждый литерал фактически создается путем вызова приватных конструкторов. Следовательно, был бы шанс передать универсальный тип конструктору во время компиляции.
Лукас Эдер
2
Сеттеры на перечислениях вполне нормальные. Особенно, если вы используете enum для создания синглтона (Item 3 Effective Java by Joshua Bloch)
Preston