Переменные и дженерики Java

Эта вещь беспокоит меня какое-то время. Я уже просил questions, но, вероятно, с плохой формулировкой и примером, который был слишком абстрактным. Поэтому было непонятно, что я на самом деле спрашивал. Я попробую еще раз. И, пожалуйста, не подходите к выводам. Я ожидаю, что ответить на вопрос нелегко!

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

Вопрос не в том, почему это невозможно, синтаксически. Я знаю, что это просто не поддерживается. Вопрос в том, почему люди JSR "забывали" или "пропускали" эту очень полезную функцию? Я не могу представить себе причину, связанную с компилятором, почему это было бы невозможно.

Вот что я хотел бы сделать. Это возможно на Java. Это способ Java 1.4 для создания типов перечислений:

// A model class for SQL data types and their mapping to Java types
public class DataType<T> implements Serializable, Comparable<DataType<T>> {
    private final String name;
    private final Class<T> type;

    public static final DataType<Integer> INT      = new DataType<Integer>("int", Integer.class);
    public static final DataType<Integer> INT4     = new DataType<Integer>("int4", Integer.class);
    public static final DataType<Integer> INTEGER  = new DataType<Integer>("integer", Integer.class);
    public static final DataType<Long>    BIGINT   = new DataType<Long>   ("bigint", Long.class);    

    private DataType(String name, Class<T> type) {
        this.name = name;
        this.type = type;
    }

    // Returns T. I find this often very useful!
    public T parse(String string) throws Exception {
        // [...]
    }

    // Check this out. Advanced generics:
    public T[] parseArray(String string) throws Exception {
        // [...]
    }

    // Even more advanced:
    public DataType<T[]> getArrayType() {
        // [...]
    }

    // [ ... more methods ... ]
}

И тогда вы можете использовать <T> во многих других местах

public class Utility {

    // Generic methods...
    public static <T> T doStuff(DataType<T> type) {
        // [...]
    }
}

Но это невозможно с перечислением:

// This can't be done
public enum DataType<T> {

    // Neither can this...
    INT<Integer>("int", Integer.class), 
    INT4<Integer>("int4", Integer.class), 

    // [...]
}

Теперь, как я уже сказал. Я знаю, что эти вещи были спроектированы именно так. enum - синтаксический сахар. Так что дженерики. На самом деле, компилятор выполняет всю работу и преобразовывает enums в подклассы java.lang.Enum и генерики в отливки и синтетические методы.

, но почему компилятор не может пойти дальше и разрешить общие перечисления?

ИЗМЕНИТЬ: Это то, что я ожидал бы в виде кода Java, созданного компилятором:

public class DataType<T> extends Enum<DataType<?>> {
    // [...]
}

Ответы

Ответ 1

Я немного угадаю и скажу, что это из-за ошибок ковариации в параметре типа самого класса Enum, который определяется как Enum<E extends Enum<E>>, хотя немного изучать все угловые случаи из этого.

Кроме того, первичный вариант использования перечислений - это такие вещи, как EnumSet и valueOf, где у вас есть коллекция вещей с разными генерическими параметрами и получить значение из строки, все из которых не будут поддерживать или ухудшать общий параметр на само перечисление.

Я знаю, что я всегда в мире боли, когда я пытаюсь понять это с помощью Generics, и я думаю, что разработчики языка заглянули в эту бездну и решили не идти туда, тем более, что функции были разработаны одновременно, что будет означать еще большую неопределенность для стороны Enum вещей.

Или иначе, у него возникнут проблемы с Class<T> при работе с классами, которые сами имеют общие параметры, и вам придется делать много кастингов и работать с необработанными типами. Не совсем то, что чувствовали разработчики языка, стоило того, на что вы смотрите.

EDIT: В ответ на комментарии (и Tom - downvote?), вложенный общий параметр делает всевозможные плохие вещи. Enum реализует Comparable. Это просто не помогло бы сравнить два произвольных элемента перечисления в клиентском коде, если бы дженерики играли. После того, как вы справитесь с параметром Generic параметра Generic, вы получите всевозможные проблемы и головные боли. Трудно создать класс, который хорошо его обрабатывает. В случае сопоставимого я не мог найти способ заставить его работать, чтобы сравнить двух произвольных членов перечисления без возврата к необработанным типам и получения предупреждения о компиляторе. Не могли бы вы?

На самом деле выше это смущающе неправильно, поскольку я использовал DataType в вопросе как мой шаблон для размышлений об этом, но на самом деле Enum будет иметь подкласс, так что это не совсем правильно.

Тем не менее, я согласен с сутью моего ответа. Том поднял EnumSet.complementOf, и, конечно же, у нас все еще есть valueOf, что создает проблемы, и в той степени, в которой дизайн Enum мог работать, мы должны понять, что это 20/20 задним числом. Enum разрабатывался одновременно с дженериками и не имел преимущества для проверки всех таких угловых случаев. Особенно учитывая, что прецедент для Enum с общим параметром весьма ограничен. (Но опять же, так же, как и для EnumSet).

Ответ 2

Я не думаю, что невозможно получить обобщенное перечисление. Если вы можете взломать компилятор, у вас может быть подкласс Enum, который является общим, а файл класса вашего общего перечисления не вызовет проблем.

Но в конце концов, перечисление в значительной степени является синтаксическим сахаром. В C, С++, С# перечисления представляют собой в основном псевдоним для int-констант. Java дает ему больше энергии, но он все еще должен представлять простые элементы.

Где-то люди должны рисовать линию. Просто потому, что класс имеет перечисленные экземпляры, не означает, что он должен быть перечислением. Если он достаточно изощрен в других областях, он заслуживает того, чтобы быть обычным классом.

В вашем случае нет большого преимущества, чтобы сделать DataType перечисление. Вы можете использовать enum в switch-case, что об этом, большое дело. Не-enum verion DataType работает просто отлично.

Ответ 3

Вот как я об этом думаю -

У обычных классов есть экземпляры. Вы создаете новый экземпляр класса, используя его для какой-либо цели, а затем удаляете его. Например, List<String> - список строк. Я могу делать то, что когда-либо хочу делать со строками, а затем, когда я закончил, я могу позже реализовать ту же функциональность с целыми числами.

Мне перечислены не типы, которые вы создаете экземпляры. Это то же самое, что и синглтон. Поэтому я могу понять, почему JAVA не разрешает генерики для Enums, потому что вы действительно не можете создать новый экземпляр типа Enum для использования временного типа, как в случае с классами. Предполагается, что перечисления являются статическими и имеют только один экземпляр во всем мире. Для меня было бы бессмысленно разрешать общие для класса, которые имеют только один экземпляр во всем мире.

Надеюсь, это поможет.

Ответ 4

Я думаю, что причина, по которой вы хотите параметризовать enum с помощью <T>, сводится к тому, чтобы иметь разные сигнатуры методов для различных констант перечисления.

В вашем примере подпись (тип параметров и тип возврата) для parse будет выглядеть следующим образом:

  • для Datatype.INT: int parse(String)
  • для Datatype.VARCHAR: String parse(String)
  • и т.д.

Итак, как бы компилятор смог проверить тип:

Datatype type = ...
...
int x = type.parse("45");

???

Чтобы применить статическую типизацию и typechecking к этому виду выражения, сигнатура метода должна быть одинаковой для всех экземпляров. Однако, в конце концов, вы предлагаете иметь разные сигнатуры методов для разных экземпляров... То почему это невозможно сделать в Java.

Ответ 5

public enum GenericEnum<T> {
  SIMPLE, COMPLEX;

  public T parse(String s) {
    return T.parse(s);
  }
}

public void doSomething() {
  GenericEnum<Long> longGE = GenericEnum<Long>.SIMPLE;
  GenericEnum<Integer> intGE = GenericEnum<Integer>.SIMPLE;

  List<Long> longList = new LinkedList<Long>();
  List<Integer> intList = new LinkedList<Integer>();

  assert(longGE == intGE);              // 16
  assert(stringList.equals(intList));   // 17

  Object x = longGE.parse("1");  // 19
}

Утверждения в строках 16 и 17 являются истинными. Общие типы недоступны во время выполнения.

Одно из преимуществ перечисления состоит в том, что вы можете использовать == для их сравнения. Утверждение в строке 16 будет равно true.

В строке 19 мы сталкиваемся с проблемой. longGE и intGE - это один и тот же объект (как показано в строке 16). Что будет получено в результате синтаксического анализа ( "1" )? Общая информация типа недоступна во время выполнения. Поэтому не было способа определить T для метода анализа во время выполнения.

Перечисления в основном статичны, они существуют только один раз. И не имеет смысла применять типичную типизацию к статическим типам.

Надеюсь, это поможет.

Примечание. Это не должно быть рабочим кодом. Он использует синтаксис, предложенный в исходном вопросе.