【问题标题】:Why shouldn't Java enum literals be able to have generic type parameters?为什么 Java 枚举文字不能具有泛型类型参数?
【发布时间】:2010-11-27 09:12:04
【问题描述】:

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

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

这个泛型类型参数&lt;T&gt; 反过来又可以在不同的地方使用。想象一个方法的泛型类型参数:

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>,
    ...
}

每个枚举字面量显然都有基于泛型类型&lt;T&gt; 的附加属性,同时又是一个枚举(不可变、单例、可枚举等)

问题:

没有人想到这一点吗?这是与编译器相关的限制吗?考虑到关键字“enum”被实现为语法糖,代表生成的代码到JVM,我不明白这个限制。

谁能给我解释一下?在你回答之前,请考虑一下:

  • 我知道泛型类型已被删除 :-)
  • 我知道有一些使用类对象的解决方法。它们是变通方法。
  • 泛型类型会在适用的情况下导致编译器生成类型转换(例如,在调用 convert() 方法时
  • 泛型类型 将位于枚举中。因此,它受每个枚举字面量的约束。因此编译器会知道,在编写String string = LITERAL1.convert(myObject); Integer integer = LITERAL2.convert(myObject); 之类的内容时应用哪种类型
  • 这同样适用于T getvalue() 方法中的泛型类型参数。编译器可以在调用String string = someClass.getValue(LITERAL1) 时应用类型转换

【问题讨论】:

  • 我也不明白这个限制。我最近遇到了这个问题,其中我的枚举包含不同的“Comparable”类型,并且使用泛型,只有相同类型的 Comparable 类型可以在没有需要抑制的警告的情况下进行比较(即使在运行时会比较正确的类型)。我本可以通过使用枚举中绑定的类型来指定支持哪种可比较类型来消除这些警告,但是我不得不添加 SuppressWarnings 注释 - 没有办法!由于 compareTo 确实抛出了一个类转换异常,我猜这没关系,但仍然......
  • (+1) 我正试图缩小项目中的类型安全漏洞,但被这个任意限制阻止了。考虑一下:将enum 变成我们在Java 1.5 之前使用的“类型安全枚举”习语。突然,您可以将枚举成员参数化。这可能就是我现在要做的。
  • @EdwinDalorzo:用来自jOOQ 的具体示例更新了问题,这在过去非常有用。
  • @LukasEder 我现在明白你的意思了。看起来很酷的一个新功能。也许你应该在project coin mailing list 中提出建议
  • 完全同意。没有泛型的枚举被削弱了。你的案例#1也是我的。如果我需要通用枚举,我会放弃 JDK5 并以普通的旧 Java 1.4 风格实现它。这种方法还有额外的好处:我不必将所有常量都放在一个类甚至包中。因此,按功能打包的样式要好得多。这对于类似配置的“枚举”来说是完美的——常量根据它们的逻辑含义分布在包中(如果我希望看到它们,我会显示类型层次结构)。

标签: java generics enums


【解决方案1】:

JEP-301 Enhanced Enums 开始讨论这个问题,很遗憾,它被撤回了。 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】:

答案就在问题中:

因为类型擦除

这两种方法都不可行,因为参数类型已被删除。

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

    ...

}

【讨论】:

  • 好吧,擦除发生在编译时。但是编译器可以使用泛型类型信息进行类型检查。然后将泛型类型“转换”为类型转换。我会改写问题
  • 不确定我是否完全遵循。采取public T convert(Object);。我猜这个方法可以例如将一堆不同的类型缩小到 ,例如是一个字符串。 String 对象的构造是运行时的——编译器不做任何事情。因此,您需要知道运行时类型,例如 String.class。还是我错过了什么?
  • 我认为你错过了泛型类型 在枚举(或其生成的类)上的事实。枚举的唯一实例是它的文字,它们都提供了一个常量泛型类型绑定。因此 public T convert(Object); 中的 T 没有歧义;
  • 啊哈!知道了。你比我聪明:)
  • 我想这归结为枚举类的评估方式与其他所有内容类似。在java 中,虽然事情似乎 是不变的,但事实并非如此。考虑带有静态块 static { FOO = "bar"; }public final static String FOO; - 即使在编译时已知,也会评估常量。
【解决方案3】:

因为你不能。严重地。这可以添加到语言规范中。它没有。它会增加一些复杂性。成本效益意味着它不是高优先级。

更新:目前正在添加到JEP 301: Enhanced Enums 下的语言中。

【讨论】:

  • 能否详细说明并发症?
  • 我已经考虑了几天了,但我仍然没有发现任何并发症。
【解决方案4】:

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

MyEnum.valueOf(String name) 呢?

对于 valueOf,如果你认为编译器可以制作像

这样的泛型方法

public static MyEnum valueOf(String name);

为了像MyEnum&lt;String&gt; myStringEnum = MyEnum.value("some string property") 那样称呼它,那也行不通。 例如,如果您拨打 MyEnum&lt;Int&gt; myIntEnum = MyEnum.&lt;Int&gt;value("some string property") 怎么办? 由于类型擦除,无法实现该方法以正常工作,例如当您像 MyEnum.&lt;Int&gt;value("some double property") 一样调用它时抛出异常或返回 null。

【讨论】:

  • 他们为什么不工作?他们只需使用通配符...:MyEnum&lt;?&gt;[] values()MyEnum&lt;?&gt; valueOf(...)
  • 但是你不能像MyEnum&lt;Int&gt; blabla = valueOf("some double property");那样做赋值,因为类型不兼容。在这种情况下,您还希望获得 null ,因为您想返回对于双属性名称不存在的 MyEnum ,并且由于擦除而无法使该方法正常工作。
  • 此外,如果您遍历 values() ,您将需要使用 MyEnum> 这通常不是您想要的,因为例如您不能仅遍历您的 Int 属性。另外你需要做很多你想避免的强制转换,我建议为每种类型创建不同的枚举或使用实例创建自己的类...
  • 好吧,我想你不能两者兼得。通常,我求助于我自己的“枚举”实现。只是Enum 有很多其他有用的功能,而这个是可选的,也非常有用...
【解决方案5】:

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

java 枚举的全部目的是对共享相似属性的类型实例的枚举进行建模,以提供超出可比较的字符串或整数表示形式的一致性和丰富性。

以教科书枚举为例。这不是很有用或一致:

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

为什么我希望我的不同行星有不同的泛型类型转换?它解决了什么问题?它是否证明使语言语义复杂化是合理的?如果我确实需要这种行为,那么枚举是实现它的最佳工具吗?

另外,您将如何管理复杂的转化?

实例

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

使用String Integer 很容易提供名称或序号。但是泛型将允许您提供任何类型。您将如何管理到MyComplexClass 的转换?现在,您通过强制编译器知道可以提供给泛型枚举的类型的有限子集并给概念(泛型)引入了似乎已经让很多程序员难以理解的额外混淆,从而混淆了两个构造。

【讨论】:

  • 想到几个没有用的例子是一个可怕的论点,它永远不会有用。
  • 例子支持了这一点。枚举实例是类型的子类(又好又简单),并且包括泛型是一堆蠕虫,它用复杂性来形容一个非常模糊的好处。如果您要对我投反对票,那么您还应该在下面对汤姆·霍廷(Tom Hawtin)投反对票,他说了同样的话,但话不多
  • @nsfyn55 枚举是 Java 最复杂、最神奇的语言特性之一。例如,没有任何其他语言功能可以自动生成静态方法。另外,每个枚举类型已经一个泛型类型的实例。
  • @nsfyn55 设计目标是让枚举成员强大而灵活,这就是为什么它们支持自定义实例方法甚至可变实例变量等高级特性。它们旨在为每个成员单独设置行为,以便他们可以作为活跃的协作者参与复杂的使用场景。最重要的是,Java 枚举旨在使一般枚举习惯用法type safe,但不幸的是,缺少类型参数使其未能实现最珍视的目标。我非常相信这是有一个非常具体的原因。
  • 我没有注意到你提到的逆变... Java 确实支持它,只是它是使用站点,这使得它有点笨拙。 interface Converter&lt;IN, OUT&gt; { OUT convert(IN in); } &lt;E&gt; Set&lt;E&gt; convertListToSet(List&lt;E&gt; in, Converter&lt;? super List&lt;E&gt;, ? extends Set&lt;E&gt;&gt; converter) { return converter.convert(in); }我们必须手动计算出每次消费哪种类型,生产哪种类型,并相应地指定界限。
【解决方案6】:

通过使用这个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 类!)。

【讨论】:

    【解决方案7】:

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

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

    【讨论】:

    • java.lang.Stringpublic static final Comparator&lt;String&gt; CASE_INSENSITIVE_ORDER。现在你可以看到了? :-)
    • 您说您看不到类型参数化常量的含义。所以我向你展示了一个类型参数化的常量(不是函数指针)。
    • 当然不是,因为java语言不知道函数指针这样的东西。尽管如此,常量不是类型参数化的,而是类型。而且类型参数本身是常量,不是类型变量。
    • 但枚举语法只是语法糖。在下面,它们就像CASE_INSENSITIVE_ORDER...如果Comparator&lt;T&gt; 是一个枚举,为什么没有与&lt;T&gt; 相关联的文字?
    • 如果 Comparator 是一个枚举,那么我们确实可以拥有各种奇怪的东西。也许像 Integer 这样的东西。但事实并非如此。
    【解决方案8】:

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

    如果 JVM 允许,你会在哪里设置 T 类?

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

    新的我的枚举()?

    以下方法可能仍然有用

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

    【讨论】:

    • 不确定我是否喜欢 enum 上的 setO() 方法。我认为枚举常量对我来说意味着不可变。所以即使有可能,我也不会这样做。
    • 枚举在编译器生成的代码中实例化。每个文字实际上是通过调用私有构造函数创建的。因此,就有机会在编译时将泛型类型传递给构造函数。
    • 枚举上的设置器是完全正常的。特别是如果您使用枚举来创建单例(Joshua Bloch 的第 3 项有效 Java)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多