Java枚举和泛型

13
这件事情困扰了我一段时间。我之前曾经提出过问题,但可能措辞有误,且示例过于抽象,所以并不清楚我的实际问题是什么。我会再试一次,请不要草率下结论。我预计这个问题很难回答!
为什么在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 ... ]
}

然后,您可以在许多其他地方使用 <T>

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), 

    // [...]
}

现在,正如我所说的。我知道这些东西是按照这种方式设计的。枚举是语法糖。泛型也是。实际上,编译器完成所有工作,并将枚举转换为java.lang.Enum的子类,将泛型转换为强制转换和合成方法。
但是,为什么编译器不能进一步允许泛型枚举呢?
编辑: 这就是我期望的编译器生成的Java代码:
public class DataType<T> extends Enum<DataType<?>> {
    // [...]
}

可能是重复的问题:为什么Java枚举字面值不能具有泛型类型参数? - Tom Hawtin - tackline
1
@Lukas Eder,所以请改进原始问题。重新提问似乎没有帮助。 - Tom Hawtin - tackline
3
我以前在stackoverflow上做过这件事,然后被批评并告知要提出新问题...你们能不能下定决心呢? :) - Lukas Eder
@Lukas 因为 Oracle 不想激怒一群客户吗?C# != JAVA - Woot4Moo
@biziclop,对不起,有误解。我不是故意冒犯的,请澄清一下。 - Lukas Eder
显示剩余7条评论
5个回答

4
我猜想这是因为Enum类本身的类型参数存在协变问题,它被定义为Enum<E extends Enum<E>>。尽管调查所有这些情况有点过分,但这是一个原因。
此外,枚举的主要用例是与EnumSet和valueOf一起使用,其中您有具有不同泛型参数的项目集,并从字符串获取值,所有这些都不支持或更糟糕地支持枚举本身的泛型参数。
当我试图在泛型方面变得复杂时,我总是处于痛苦之中。我想象语言设计师们窥视了那个深渊并决定不去那里,特别是因为这些功能是同时开发的,这意味着对于枚举方面的事情,甚至会更加不确定。
或者换句话说,它将具有处理具有自身泛型参数的类所涉及的所有问题的Class<T>问题,并且您将不得不进行大量的强制转换和处理原始类型。对于您正在查看的用例类型,语言设计师认为这并不真正值得。
编辑:响应评论(和Tom-否定票?),嵌套泛型参数会导致各种问题。枚举实现了Comparable。如果泛型参与比较枚举的两个任意元素,则在客户端代码中将无法运行。一旦您处理了具有泛型参数的泛型参数,就会遇到各种限制问题和头痛问题。很难设计一个能够很好处理它的类。在可比较的情况下,我无法想出一种方法来比较枚举的两个任意成员,而不是回到原始类型并获得编译器警告。你可以吗?
实际上,以上内容非常错误,因为我正在使用问题中的DataType作为我的思考模板,但实际上Enum将具有一个子类,所以那不完全正确。
但是,我仍然坚持我的答案要点。Tom提出了EnumSet.complementOf,当然我们仍然有valueOf会产生问题,以及在Enum的设计可能有效的程度上,我们必须意识到这是一个20/20后见之明的事情。Enum正在与泛型同时设计,并没有验证所有这些边角情况的好处。特别是考虑到带有泛型参数的枚举的用例相当有限。(但是,同样适用于EnumSet的用例)。

非常好的答案。我很少使用EnumSet。我可以看出它们会妨碍我的<T>泛型类型。 - Lukas Eder
一个涉及到类泛型的奇怪例子是Enum.valueOf方法。它的第一个参数应该是枚举类。但现在你有一个问题:如果你传递Class<DataType>,你会得到一个原始类型警告,并且你不能传递Class<DataType<Integer>>(如果你能以类型安全的方式获得它,那意味着什么?)。 - waxwing
我看不到任何有意义的东西。 - Tom Hawtin - tackline
class En<T extends En<T>> { } class DateType<T> extends En<DateType<T>> { } class IntegerDateType extends DateType<Integer> { } 对我来说可以工作得很好。至少到目前为止是这样的。我们会有 EnumSet<DataType<?>>,虽然我们还有 EnumSet<DataType<Integer>>。不幸的是,当我们使用 EnumSet.complementOf 或者 EnumSet.range 时,最后一个会有点棘手。 - Tom Hawtin - tackline
我在这里错过了一些东西... 我想你们并没有取得太大进展,除了不确定为什么枚举不是那样设计... 或许我们永远不会知道 :-) 祝你周末愉快! - Lukas Eder
显示剩余5条评论

1

我认为拥有泛型枚举并不是不可能的。如果你能够入侵编译器,你可以拥有一个泛型的 Enum 子类,而你的泛型枚举的类文件也不会引起问题。

但最终,枚举基本上只是一种语法糖。在 C、C++、C# 中,枚举基本上是 int 常量的别名。Java 给了它更多的力量,但它仍然应该表示简单的项目。

人们必须划定界限。仅仅因为一个类具有枚举实例,并不意味着它必须是一个枚举。如果它在其他领域足够复杂,那么它应该成为一个普通的类。

在你的情况下,将 DataType 设为枚举并没有太多优势。你可以在 switch-case 中使用枚举,这就是全部。非枚举版本的 DataType 也可以正常工作。


我理解你的观点。但是,实现equals()hashCode()toString()SerializableComparablevalues()valueOf()等方法对于枚举类型来说也是一个很大的优势。这通常是我选择枚举类型而不仅仅是使用switch-case语句的原因。 - Lukas Eder

0

我认为你希望使用<T>参数化枚举的原因在于能够为枚举的各个常量拥有不同的方法签名。

以你的示例为例,parse的签名(参数类型和返回类型)将是:

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

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

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

???

应用静态类型和类型检查到这种表达式,方法的签名必须在所有实例中都相同。然而,最终你建议为不同的实例使用不同的方法签名......这就是为什么无法在Java中实现的原因。

请注意:编译器会对您的示例进行以下操作:int x = ((Integer) type.parse("45")).intValue() - Lukas Eder
在1.4版本中,您有不同类型的“常量”(实际上是实例):INT是Datatype <Integer>类型,VARCHAR是Datatype <String>类型等。这些类型彼此不兼容。而且,这与枚举的定义(理解用例)不兼容,因为常量必须是相同类型的! - ChrisJ
1
Datatype有一个泛型参数(或将有),因此Datatype<Integer> type = ... - Tom Hawtin - tackline
是的,我知道。但是为什么它们必须与Java <1.5理解的相同类型**和**与java.lang.Enum <?>理解的相同通用类型?为什么?我对这个问题感到疯狂。 - Lukas Eder
@Tom:枚举的重点在于拥有一堆相同类型的常量,对吧?而你提出的方案会破坏这个要求,因为枚举的常量将是不同类型的... - ChrisJ
显示剩余6条评论

0

这是我的想法 -

普通类有实例。您创建一个类的新实例,用于某些目的,然后处理它。例如,List<String> 是字符串列表。我可以随心所欲地使用字符串,然后在完成后可以使用整数执行相同的功能。

对我来说,枚举器不是您创建实例的类型。这与单例模式相同。因此,我可以理解为什么JAVA不允许枚举类型使用泛型,因为您无法像使用类一样临时创建枚举类型的新实例。枚举应该是静态的,并且全局只有一个实例。对我来说,允许为仅具有全局唯一实例的类使用泛型没有意义。

希望这可以帮助到您。


@Amir Raminfar 那里有三个 Color 实例:redgreenwhite - Tom Hawtin - tackline
1
对,我不是想要三个Color的实例,而是一个Red的实例。抱歉让你产生了困惑。 - Amir Raminfar
我猜你的意思是你应该能够创建枚举类型 Color<T> {Red<Integer>, Green<String>}。我可以看出这会很有用。 - Amir Raminfar
@Amir。没错,我想这么做。虽然颜色类型似乎没有什么用处,但我认为我的例子是一个更加现实的用例。 - Lukas Eder
谢谢。我真的认为我有些头绪了!如果在定义之后不能创建新实例,那么泛型有什么意义呢?也许我们可以在JDK7中加入这个功能 :) - Amir Raminfar
显示剩余8条评论

-1
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。
枚举基本上是静态的,它们只存在一次。对于静态类型,将泛型类型应用于其并没有意义。 希望这有所帮助。
注意- 这并不是要实际工作的代码。它使用原始问题中建议的语法。

1
嗯,我认为这不会起作用:... = GenericEnum<Long>.SIMPLE,因为GenericEnum.SIMPLE是一个常量。它的泛型类型 <T> 已经被绑定(即 SIMPLE 实际上是 public static final GenericEnum SIMPLE;),不能再更改了。你可以进行不安全的转换,即 GenericEnum<Long> longGE = (GenericEnum<Long>) GenericEnum.SIMPLE;,但正如我所说,这是一种不安全的转换... - Lukas Eder

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