【问题标题】:Java enums and genericsJava 枚举和泛型
【发布时间】:2011-02-24 18:14:37
【问题描述】:

这件事让我困扰了一段时间。我之前问过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 ... ]
}

然后,您可以在许多其他地方使用&lt;T&gt;

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<?>> {
    // [...]
}

【问题讨论】:

  • 好吧,我说我再次问了这个问题,但措辞更清晰,因为另一个问题只是给了我垃圾答案(在我看来)。我宁愿关闭另一个...
  • @Lukas Eder 所以改进原始问题。重新提问似乎没有帮助。
  • 这不是可行性问题,而是这实际上会破坏部署的问题?另外,你为什么需要这个功能?
  • 我之前在stackoverflow上做过,然后我被批评并被告知要问一个新问题......你们能下定决心吗? :)

标签: java generics enums


【解决方案1】:

我会猜测一下,说这是因为 Enum 类本身的类型参数的协方差问题,它被定义为Enum&lt;E extends Enum&lt;E&gt;&gt;,尽管调查所有极端情况有点多那个。

除此之外,枚举的主要用例是 EnumSet 和 valueOf 之类的东西,其中你有一组具有不同泛型参数的东西并从字符串中获取值,所有这些都不支持或更糟的泛型参数枚举本身。

我知道,当我尝试对泛型进行幻想时,我总是处于痛苦的世界中,我想语言设计者看到了这个深渊并决定不去那里,特别是因为这些功能是同时开发的,这对 Enum 方面来说意味着更多的不确定性。

或者换一种说法,在处理本身具有泛型参数的类时,它会遇到Class&lt;T&gt; 的所有问题,并且您必须进行大量转换和处理原始类型。对于您正在查看的用例类型,语言设计人员认为这并不真正值得。

编辑:为了响应 cmets(以及 Tom - 否决票?),嵌套的泛型参数会导致各种坏事发生。枚举实现 Comparable。如果泛型在起作用,那根本无法比较客户端代码中枚举的两个任意元素。一旦你处理了一个泛型参数的泛型参数,你最终会遇到各种边界问题和令人头疼的问题。很难设计一个能很好地处理它的类。在可比较的情况下,我想不出一种方法来比较枚举的两个任意成员而不恢复为原始类型并获得编译器警告。可以吗?

实际上上面的内容是非常错误的,因为我使用问题中的 DataType 作为我的模板来思考这个问题,但实际上 Enum 会有一个子类,所以这不太正确。

但是,我坚持我的回答的要点。 Tom 提出了EnumSet.complementOf,当然我们仍然有valueOf 产生问题,并且就 Enum 的设计可以工作的程度而言,我们必须意识到这是一个 20/20 的事后诸葛亮。枚举是与泛型同时设计的,并没有验证所有这些极端情况的好处。特别是考虑到具有通用参数的 Enum 的用例相当有限。 (但话又说回来,EnumSet 的用例也是如此)。

【讨论】:

  • +1 非常好的答案。我几乎不使用EnumSet。我可以看到它们会妨碍我的 &lt;T&gt; 泛型类型
  • Enum.valueOf 方法是一个奇怪的示例(涉及 Class 是泛型的)。它的第一个参数应该是枚举类。但是现在你有一个问题:如果你传递它 Class,你会得到一个原始类型警告,你不能传递它 Class>以一种类型安全的方式?)。
  • 我看不到任何有意义的东西。
  • 我又想到了这个问题。 public enum DataType&lt;T&gt; 生成到public class DataType&lt;T&gt; extends Enum&lt;DataType&lt;T&gt;&gt; 是错误的,因为&lt;T&gt; 可以绑定到每个实例,Enum&lt;E&gt; 不支持。不过这可能有效:public class DataType&lt;T&gt; extends Enum&lt;DataType&lt;?&gt;&gt;。当E 绑定到DataType&lt;?&gt; 时,我认为EnumSet 没有问题...
  • class En&lt;T extends En&lt;T&gt;&gt; { } class DateType&lt;T&gt; extends En&lt;DateType&lt;T&gt;&gt; { } class IntegerDateType extends DateType&lt;Integer&gt; { } 对我来说效果很好。至少那么远。尽管我们也有EnumSet&lt;DataType&lt;Integer&gt;&gt;,但我们将有半有用的EnumSet&lt;DataType&lt;?&gt;&gt;。不幸的是,当我们使用EnumSet.complementOfEnumSet.range 时,最后一个变得有点问题。
【解决方案2】:

我不认为有泛化枚举是不可能的。如果你可以破解编译器,你可以拥有一个通用的 Enum 子类,并且你的通用枚举的类文件不会导致问题。

但归根结底,枚举几乎是一种语法糖。在 C、C++、C# 中,枚举基本上是 int 常量的别名。 Java 赋予了它更多的功能,但它仍然应该表示简单的项目。

人们必须在某个地方划清界限。仅仅因为一个类具有枚举实例,并不意味着它必须是一个枚举。如果它在其他领域足够成熟,它应该是一个常规课程。

在您的情况下,将 DataType 设为枚举并没有太大优势。您可以在 switch-case 中使用枚举,仅此而已,很重要。 DataType 的非枚举版本可以正常工作。

【讨论】:

  • 我明白你的意思。但是,实现equals()hashCode()toString()SerializableComparablevalues()valueOf() 等对enum 来说也是一大优势。这通常是我选择enum 的原因,而不仅仅是switch-case 声明。
【解决方案3】:

我是这么想的——

常规类有实例。您创建一个类的新实例,将其用于某种目的,然后将其处理掉。例如List&lt;String&gt; 是一个字符串列表。我可以用字符串做任何我想做的事情,然后当我完成后,我可以稍后用整数做同样的功能。

对我来说,枚举器不是您创建实例的类型。它与单例相同。所以我明白为什么 JAVA 不允许 Enums 的泛型,因为你真的不能创建一个 Enum 类型的新实例来使用临时的,就像你对类所做的那样。枚举应该是静态的,并且全局只有一个实例。对我来说,为一个全局只有一个实例的类允许泛型是没有意义的。

我希望这会有所帮助。

【讨论】:

  • 这对我来说确实有意义。我提供了一个很好的例子来说明我为什么要这样做。它适用于创建类型安全枚举的经典方式。但不是enum 方式。静态实例仍然是实例。为什么它不应该有一个泛型类型参数?
  • @Tom - 你是什么意思?如果我创建一个枚举 Color{red, green, white} 系统中只有一个 Color.white 。我已经声明了一次 Color 并且只有一个 Color 实例。
  • @Amir Raminfar 这里有Color 的三个实例:redgreenwhite
  • 没有Color 的三个实例。查看生成的字节码。他们是public static final Color 实例!
  • 正确,我不是指三个颜色实例,我是指一个红色实例。很抱歉造成混乱。
【解决方案4】:

我认为您希望使用 &lt;T&gt; 参数化枚举的原因归结为能够为枚举的各种常量提供不同的方法签名。

在您的示例中,parse 的签名(参数类型和返回类型)将是:

  • 对于Datatype.INTint parse(String)
  • 对于Datatype.VARCHARString parse(String)
  • 等等

那么编译器如何能够对以下内容进行类型检查:

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

???

要对这种表达式应用静态类型和类型检查,方法的签名必须对所有实例都相同。但是,最后您建议为不同的实例使用不同的方法签名......这就是为什么在 Java 中无法做到这一点。

【讨论】:

  • 然后告诉我这如何以 1.4 方式实现类型安全枚举(是的,这是可能的),但不是 enum
  • 注:编译器对您的示例执行此操作:int x = ((Integer) type.parse("45")).intValue()
  • 在 1.4 中,您有 不同类型 的“常量”(实际上是实例):INT 是 Datatype 类型,VARCHAR 是 Datatype 类型等等。这些类型彼此不兼容。这与枚举的定义(理解用例)不兼容,枚举的常量必须是相同的类型!
  • Datatype 有一个通用参数(或会),所以Datatype&lt;Integer&gt; type = ...
  • @Lukas,是的,您对拆箱的看法是对的,但这不是我的意思。我的观点是:“编译器怎么知道 parse 返回一个整数?”实际上,它可以是 Long、String 等。而编译器没有这个信息。
【解决方案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 行的断言所示。) parse("1") 将返回什么?泛型类型信息在运行时不可用。因此无法在运行时确定 parse 方法的 T。

枚举基本上是静态的,它们只存在一次。而且将泛型类型应用于静态类型是没有意义的。

我希望这会有所帮助。

注意 - 这不是工作代码。它使用原始问题中建议的语法。

【讨论】:

  • 嗯,我认为这行不通:... = GenericEnum&lt;Long&gt;.SIMPLE,因为GenericEnum.SIMPLE 是一个常数。它的泛型类型&lt;T&gt; 最终被绑定(即SIMPLE 实际上是public static final GenericEnum SIMPLE;)并且不能再更改了。你可以不安全地投射它,即GenericEnum&lt;Long&gt; longGE = (GenericEnum&lt;Long&gt;) GenericEnum.SIMPLE;,但正如我所说,这是一个不安全的投射......
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多