为什么Java枚举字面量不能具有泛型类型参数?

176

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无论如何都会抛出类转换异常,所以我想这没关系,但仍然有点不爽... - MetroidFan2002
5
我正在尝试解决项目中的类型安全问题,但被这个任意限制卡住了。你可以考虑将 enum 转换成我们在 Java 1.5 之前使用的 "类型安全枚举" 惯用法。这样一来,你就可以将枚举成员参数化了。我现在可能会这么做。 - Marko Topolnik
1
@EdwinDalorzo:更新了问题,并提供了一个具体的例子来自 jOOQ,在过去这将会非常有用。 - Lukas Eder
2
@LukasEder 我现在明白你的观点了。看起来是一个很酷的新功能。也许你应该在 project coin mailing list 中提出建议。我在那里看到其他有趣的枚举提案,但没有像你的这样的。 - Edwin Dalorzo
1
完全同意。没有泛型的枚举是残缺的。你的情况#1也是我的情况。如果我需要泛型枚举,我放弃JDK5并以旧的Java 1.4样式实现它。这种方法还有额外的好处:我不被迫将所有常量放在一个类或甚至包中。因此,按功能划分的包风格得到了更好的实现。这对于类似配置的“枚举”非常完美-常量根据其逻辑含义在包中分散(如果我希望看到它们全部,我可以显示类型层次结构)。 - Tomáš Záluský
显示剩余9条评论
8个回答

74

这个问题已经在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


4
截至2018年12月,围绕JEP 301的一些迹象再次出现,但是浏览该讨论可以明显看出问题仍然远未解决。 - Pont
2
该JEP已于2020年9月被撤回:撤回评论 - M. Justin

17
答案就在问题里:
因为类型擦除。
由于参数类型被擦除,这两种方法都不可行。
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
好的,擦除是在编译时发生的。但编译器可以使用泛型类型信息进行类型检查。然后将泛型类型“转换”为类型转换。我会重新表述问题。 - Lukas Eder
2
我不太确定是否理解了。public T convert(Object);。我猜测这个方法可以将许多不同类型的对象转换为<T>,例如String。String对象的构建是在运行时完成的 - 编译器无法做到这一点。因此,您需要知道运行时类型,例如String.class。或者我有什么遗漏吗? - Martin Algesten
8
我认为你忽略了泛型类型<T>在枚举(或其生成的类)上的事实。枚举的唯一实例是它的字面量,它们都提供了一个常量泛型类型绑定。因此,在public T convert(Object)中,T没有任何歧义。 - Lukas Eder
2
啊哈!懂了。你比我聪明 :) - Martin Algesten
1
我想这归结于枚举类被以与其他所有类似的方式进行评估。在Java中,尽管事物看起来是常量,但它们并不是。考虑public final static String FOO;和一个静态块static { FOO = "bar"; } - 即使在编译时已知常量,也会对常量进行评估。 - Martin Algesten
1
这两种方法都不可行。那么它们可行吗? - Mr_and_Mrs_D

5

在枚举中,还有其他方法是行不通的。 MyEnum.values() 会返回什么?

那么 MyEnum.valueOf(String name) 呢?

对于 valueOf 方法,如果你认为编译器可以生成类似于

public static 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> 并不存在于 double 属性名称中,而且你无法使该方法正常工作,因为被擦除了。 - user1944408
此外,如果您要循环遍历values(),则通常不需要使用MyEnum<?>,因为例如您无法仅遍历Int属性。此外,您需要进行大量的强制转换,而这是要避免的。我建议为每种类型创建不同的枚举或创建自己的类实例... - user1944408
嗯,我想你不能两全其美。通常情况下,我会采用自己的“枚举”实现。只是 Enum 有很多其他有用的功能,而这个是可选的,也非常有用... - Lukas Eder

5
因为你不能这样做。说真的,这可能会被添加到语言规范中,但目前还没有这样做。这将增加一些复杂性。收益与成本之间的权衡意味着这不是一个高优先级的问题。
更新:当前正在JEP 301:增强枚举下添加到语言中。

1
你能详细说明一下这些复杂情况吗? - Mr_and_Mrs_D
5
我已经思考了几天,但我仍然没有看到任何复杂性。 - Marko Topolnik

1
通过使用这个Java注解处理器https://github.com/cmoine/generic-enums,你可以编写类似于以下内容的代码(将convert方法作为示例实现了):
import org.cmoine.genericEnums.GenericEnum;
import org.cmoine.genericEnums.GenericEnumParam;

@GenericEnum
public enum MyEnum {
    LITERAL1(String.class) {
        @Override
        @GenericEnumParam
        public Object convert(Object o) {
            return o.toString(); // example
        }
    },
    LITERAL2(Integer.class) {
        @Override
        @GenericEnumParam
        public Object convert(Object o) {
            return o.hashCode(); // example
        }
    },
    LITERAL3(Object.class) {
        @Override
        @GenericEnumParam
        public Object convert(Object o) {
            return o; // example
        }
    };

    MyEnum(Class<?> clazz) {
    }

    @GenericEnumParam
    public abstract Object convert(Object o);
}

注解处理器将生成一个枚举类MyEnumExt(可自定义),它克服了Java枚举的限制。相反,它生成一个可以完全像枚举一样使用的Java类(最终,枚举被编译成实现Enum接口的Java类!)。

-2

坦白地说,这似乎更像是在寻找问题的解决方案。

Java枚举的整个目的是以一种提供比可比较的字符串或整数表示更一致和丰富的方式对类型实例的枚举进行建模。

以教科书枚举为例。这并不是非常有用或一致的:

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

为什么我希望我的不同星球具有不同的通用类型转换?这解决了什么问题?它是否证明了语言语义的复杂性?如果我确实需要这种行为,那么枚举是实现它的最佳工具吗?
此外,您将如何管理复杂的转换?
例如:
public enum BadIdea<T>{
   INSTANCE1<Long>,
   INSTANCE2<MyComplexClass>;
}

使用String Integer很容易提供名称或序数。但是泛型允许您提供任何类型。您如何管理转换为MyComplexClass?现在,通过强制编译器知道可以提供哪些有限子集的类型来弄乱两个结构,并向已经似乎难以理解的概念(泛型)引入额外的混淆。


20
举出几个例子表明某事不实用是一个证明其永远不实用的可怕论点。 - Elias Vasylenko
1
这些例子支持了这一点。枚举实例是类型的子类(简单明了),包括泛型会增加复杂性,而收益却非常微不足道。如果你要给我投反对票,那么你也应该给Tom Hawtin投反对票,因为他用不太多的话说了同样的事情。 - nsfyn55
1
@nsfyn55 枚举是 Java 中最复杂、最神奇的语言特性之一。没有其他语言特性能够自动生成静态方法。此外,每个枚举类型已经是一个泛型类型的实例。 - Marko Topolnik
1
@nsfyn55 设计目标是使枚举成员强大而灵活,这就是为什么它们支持诸如自定义实例方法甚至可变实例变量等高级功能。它们被设计为具有针对每个成员专门定制的行为,因此它们可以作为复杂使用场景中的积极协作者参与其中。最重要的是,Java枚举旨在使一般枚举习语类型安全,但缺乏类型参数使其无法达到最珍视的目标。我相信这背后有一个非常具体的原因。 - Marko Topolnik
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
显示剩余12条评论

-3
因为"enum"是枚举的缩写。 它只是一组命名常量,用于代替序数,使代码更易读。
我不明白类型参数化常量的预期含义是什么。

1
java.lang.String 中:public static final Comparator<String> CASE_INSENSITIVE_ORDER。你现在明白了吗? :-) - Lukas Eder
4
你说你不明白有类型参数的常量有什么意义。所以我给你展示了一个有类型参数的常量(而不是一个函数指针)。 - Lukas Eder
当然不可以,因为Java语言不支持函数指针这种概念。不过,常量并非类型参数化,而是其类型。而且,类型参数本身是常量,不是类型变量。 - Ingo
但枚举语法只是语法糖。在底层,它们与CASE_INSENSITIVE_ORDER完全相同...如果Comparator<T>是一个枚举,为什么不有带有任何关联绑定的文字字面量 <T> 呢? - Lukas Eder
如果Comparator<T>是一个枚举类型,那么我们可能会有各种奇怪的事情发生。也许会出现类似于Integer<T>这样的东西。但它并不是枚举类型。 - Ingo
请查看更新的问题。我添加了一个更贴近生活的例子来说明我的用例。 - Lukas Eder

-4

我认为这是因为枚举基本上不能被实例化。

如果JVM允许您这样做,您会在哪里设置T类?

枚举是始终应该保持不变的数据,或者至少不会动态更改的数据。

new 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;
    }   
}

9
我不确定我是否喜欢在枚举类型中使用setO()方法。我认为枚举类型是常量,这意味着它们是不可变的。因此,即使可能,我也不会这样做。 - Martin Algesten
2
枚举在编译器生成的代码中进行实例化。每个字面量实际上都是通过调用私有构造函数创建的。因此,在编译时将有机会将通用类型传递给构造函数。 - Lukas Eder
2
枚举类型上的 setter 是非常正常的。特别是如果您正在使用枚举类型来创建单例 (参考 Joshua Bloch 的《Effective Java》中的第 3 条建议)。 - Preston

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接