ChatGPT解决这个技术问题 Extra ChatGPT

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

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,我不理解这个限制。

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

我知道泛型类型被删除了:-)

我知道有使用 Class 对象的解决方法。它们是解决方法。

泛型类型会在适用的情况下导致编译器生成的类型转换(例如,在调用 convert() 方法时)

泛型类型 将在枚举上。因此,它受每个枚举字面量的约束。因此编译器会知道,在编写类似 String string = LITERAL1.convert(myObject); 之类的内容时应用哪种类型。整数整数 = 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 风格实现它。这种方法还有额外的好处:我不必将所有常量都放在一个类甚至包中。因此,按功能打包的样式要好得多。这对于类似配置的“枚举”来说是完美的——常量根据它们的逻辑含义分布在包中(如果我希望看到它们全部,我会显示类型层次结构)。

L
Lukas Eder

这已在 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


截至 2018 年 12 月,有一些 signs of life around JEP 301 again,但略读该讨论可以清楚地表明,这些问题仍远未解决。
此 JEP 已于 2020 年 9 月撤销:withdrawing comment
M
Martin Algesten

答案在问题中:

因为类型擦除

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

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);。我猜这个方法可以例如将一堆不同的类型缩小到<T>,例如是一个字符串。 String 对象的构造是运行时的——编译器不做任何事情。因此,您需要知道运行时类型,例如 String.class。还是我错过了什么?
我认为您错过了泛型类型 在枚举(或其生成的类)上的事实。枚举的唯一实例是它的文字,它们都提供了一个常量泛型类型绑定。因此 public T convert(Object); 中的 T 没有歧义;
啊哈!知道了。你比我聪明:)
我想这归结为以与其他所有内容类似的方式评估枚举的类。在java 中,虽然事情似乎 是不变的,但事实并非如此。考虑带有静态块 static { FOO = "bar"; }public final static String FOO; - 即使在编译时已知常量也会被评估。
T
Tom Hawtin - tackline

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

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


你能详细说明并发症吗?
我已经考虑了几天了,我仍然没有看到任何并发症。
u
user1944408

ENUM 中还有其他方法不起作用。 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。


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

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

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

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

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 枚举旨在使通用枚举惯用语类型安全,但不幸的是,缺少类型参数使其未能实现最珍视的目标。我非常相信这是有一个非常具体的原因。
我没有注意到您提到逆变... 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); } 我们必须手动计算出每次使用哪种类型以及生产哪种类型,并相应地指定界限。
C
Christophe Moine

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


I
Ingo

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

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


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

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

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

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