为什么Java枚举文字不能具有通用类型参数?


148

Java枚举很棒。泛型也是如此。当然,由于类型擦除,我们都知道后者的局限性。但是有一件事我不理解,为什么我不能创建这样的枚举:

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

这样,该通用类型参数<T>又可以在各个地方使用。想象一下方法的通用类型参数:

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

甚至在枚举类本身中:

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
我也不明白这个限制。最近,我遇到了这个问题,我的枚举包含不同的“可比较”类型,而对于泛型,只能比较相同类型的可比较类型,而无需删除警告(即使在运行时会比较适当的警告)。通过使用枚举中绑定的类型来指定支持哪种可比较类型,我本可以摆脱这些警告,但是我不得不添加SuppressWarnings注释-没办法!由于compareTo确实会引发类
强制转换

5
(+1)我正试图弥合我的项目中的类型安全差距,但由于这种任意限制而停滞不前。只需考虑一下:将enumJava变成Java 1.5之前使用的“类型安全的枚举”惯用法。突然,您可以将枚举成员参数化。那可能就是我现在要做的。
Marko Topolnik 2015年

1
@EdwinDalorzo:用jOOQ的一个具体示例更新了问题,在过去这在过去非常有用。
卢卡斯·埃德

2
@LukasEder我明白你的意思了。看起来很酷的一项新功能。也许您应该在项目硬币邮寄清单中提出建议,我在枚举中看到了其他有趣的提议,但没有一个像您的提议那样。
Edwin Dalorzo'3

1
完全同意。没有泛型的枚举会瘫痪。您的案例#1也是我的。如果需要通用枚举,则放弃JDK5并以普通的旧Java 1.4样式实现它。这种方法还具有其他好处:我不被迫将所有常量都放在一个类甚至包中。因此,按功能打包样式可以更好地完成。这对于配置类的“枚举”来说是完美的-常数根据其逻辑含义散布在包中(如果我希望看到它们的全部,我将显示类型层次结构)。
托马什Záluský

Answers:


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年12月,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.class。还是我错过了什么?
Martin Algesten 2010年

6
我认为您缺少通用类型<T>在枚举(或其生成的类)上的事实。枚举的唯一实例是其文字,它们都提供恒定的泛型类型绑定。因此,在公共T convert(Object)中没有关于T的歧义。
卢卡斯·埃德

2
啊哈!得到它了。您比我聪明:)
Martin Algesten

1
我想这归结于类的枚举被评估以类似于其他所有方式。在Java中,尽管情况似乎是不变的,但事实并非如此。考虑public final static String FOO;使用静态块static { FOO = "bar"; }-即使在编译时已知常量,也要对其求值。
Martin Algesten 2010年


4

ENUM中还有其他方法不起作用。什么会MyEnum.values()回来?

MyEnum.valueOf(String name)

对于valueOf,如果您认为编译器可以使通用方法像

公共静态MyEnum valueOf(String name);

为了将其命名为MyEnum<String> myStringEnum = MyEnum.value("some string property"),这也不起作用。例如,如果您打电话MyEnum<Int> myIntEnum = MyEnum.<Int>value("some string property")怎么办?无法实现该方法以使其正常工作,例如,MyEnum.<Int>value("some double property")由于类型擦除而在引发调用时抛出异常或返回null 。


2
他们为什么不工作?他们只是使用通配符...:MyEnum<?>[] values()MyEnum<?> valueOf(...)
Lukas Eder

1
但是,MyEnum<Int> blabla = valueOf("some double property");由于类型不兼容,您无法进行这种分配。另外,在这种情况下,您还希望获得null,因为您想返回MyEnum <Int>(对于双属性名不存在),并且由于擦除而无法使该方法正常工作。
user1944408 '02

同样,如果循环遍历values(),则需要使用MyEnum <?>,这通常不是您想要的,因为例如,您无法仅遍历Int属性。另外,您还需要避免很多转换,我建议为每种类型创建不同的枚举,或者使用实例创建自己的类……
user1944408 2015年

好吧,我想你们不可能两者兼得。通常,我求助于自己的“枚举”实现。只是它Enum具有许多其他有用的功能,而这个功能是可选的,也非常有用...
Lukas Eder 2015年

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?现在,通过迫使编译器知道可以提供给泛型枚举的类型的有限子集,并对概念(泛型)引入更多的混淆,这似乎已经使许多程序员望而却步,从而弄糟了两个构造。


17
想到其中无用的几个例子是一个可怕的论点,即它永远不会有用。
Elias Vasylenko 2013年

1
这些例子证明了这一点。枚举实例是类型的子类(不错且很简单),而包含泛型的实例就是蠕虫的罐头,它的复杂性带来了非常模糊的好处。如果您要对我
投反对票,

1
@ nsfyn55枚举是Java最复杂,最神奇的语言功能之一。例如,没有其他语言功能可以自动生成静态方法。另外,每个枚举类型已经泛型类型的实例。
Marko Topolnik

1
@ nsfyn55设计目标是使枚举成员强大而灵活,这就是为什么它们支持诸如自定义实例方法甚至可变实例变量之类的高级功能。他们被设计为具有专门针对每个成员的行为,因此他们可以作为积极的协作者参与复杂的使用场景。最重要的是,Java枚举被设计为使通用枚举惯用语类型安全,但是不幸的是,缺少类型参数使它无法实现最受人尊敬的目标。我完全相信有一个非常具体的原因。
Marko Topolnik 2015年

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); }每次使用和产生哪种类型时,我们都必须手动计算,并相应地指定范围。
Marko Topolnik

-2

因为“枚举”是枚举的缩写。它只是一组命名常量,代替序数以使代码更易读。

我看不到类型参数化常量的预期含义是什么。


1
java.lang.String:中public static final Comparator<String> CASE_INSENSITIVE_ORDER。现在你可以看到了?:-)
卢卡斯·埃德

4
您说过您看不到类型参数化常量的含义。因此,我向您展示了一个类型参数化的常量(而不是函数指针)。
卢卡斯·埃德

当然不是,因为Java语言不知道诸如函数指针之类的东西。尽管如此,常量不是参数化类型,而是参数类型。而且类型参数本身是常量,而不是类型变量。
Ingo

但是枚举语法只是语法糖。在下面,它们就像CASE_INSENSITIVE_ORDER...如果Comparator<T>是一个枚举,为什么不带任何带有绑定的文字<T>呢?
卢卡斯·埃德

如果Comparator <T>是一个枚举,那么实际上我们可能有各种各样的怪异事物。也许像Integer <T>之类的东西。但事实并非如此。
Ingo

-3

我认为因为基本上无法列举Enums

如果JVM允许,您将在哪里设置T类?

枚举是指应该始终相同或至少不会发生动态变化的数据。

新的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。我认为枚举常量对我来说意味着不可变的。因此,即使有可能,我也不会这样做。
Martin Algesten

2
在编译器生成的代码中实例化枚举。每个文字实际上都是通过调用私有构造函数来创建的。因此,将有机会在编译时将泛型传递给构造函数。
卢卡斯·埃德

2
枚举的二传手是完全正常的。特别是如果您使用枚举创建一个单例(Joshua Bloch的第3项有效Java)
Preston
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.