Java的枚举相比于旧的“类型安全枚举”模式有哪些优势?

32
在 JDK1.5 之前的 Java 版本中,"类型安全枚举"模式是实现只能取有限数量值的类型的常见方式:
public class Suit {
    private final String name;

    public static final Suit CLUBS =new Suit("clubs");
    public static final Suit DIAMONDS =new Suit("diamonds");
    public static final Suit HEARTS =new Suit("hearts");
    public static final Suit SPADES =new Suit("spades");    

    private Suit(String name){
        this.name =name;
    }
    public String toString(){
        return name;
    }
}

(例如参考 Bloch 的 Effective Java 第21条)。

现在在JDK1.5+中,“官方”的方式显然是使用enum

public enum Suit {
  CLUBS("clubs"), DIAMONDS("diamonds"), HEARTS("hearts"), SPADES("spades");

  private final String name;

  private Suit(String name) {
    this.name = name;
  }
}

显然,语法更为简洁和优美(不需要为值明确定义字段,提供了合适的 toString()),但到目前为止,enum 看起来与 Typesafe Enum 模式非常相似。

我所知道的其他区别包括:

  • enum 自动提供一个 values() 方法
  • enum 可以在 switch() 中使用(编译器甚至检查您是否遗漏了某个值)

但这些看起来只是一些语法糖,并且还有一些限制(例如 enum 总是继承自 java.lang.Enum,不能被子类化)。

除了 Typesafe Enum 模式无法实现的其他更深层次的好处吗?


真正的优势在于序列化更快。其余部分可以模拟。 - bestsss
@sleske,你说的“编译器在switch语句中进行检查”是什么意思?我从未听说过这个功能。最好不要使用switch,而是为抽象方法的每个枚举值实现一个具体方法。 - human.js
1
如果在switch语句中重复使用case,编译器将会显示错误,并且如果没有所有枚举值被case覆盖,它也可以发出警告。是的,通常最好使用多态而不是switch,但这取决于具体情况。 - sleske
@sleske 感谢您澄清这一点。 - human.js
7个回答

16
  • "不能被子类化"并不是一种限制,相反这是其一个重要的优点:确保enum中定义的值集合始终恰好只有这些值,没有更多!
  • enum能够正确地处理序列化。对于类型安全的枚举也可以实现,但这经常被忽视(或者根本不知道)。这确保了e1.equals(e2)始终意味着e1==e2,对于任何两个enume1e2都成立(反之亦然,后者可能更重要)。
  • 还有特定的轻量级数据结构来处理枚举:EnumSetEnumMap(摘自这个回答

我认为你无法通过子类化一个仅有私有构造函数的类来实现,除非嵌套在该类本身内部。 - Mark Peters
@Mark:没错,但你可以修改这个类,将构造函数改为protected - Joachim Sauer
是的,我认为构造函数应该是私有的,类应该是final的。 - Mark Peters
+1序列化。这也是防止在运行时向枚举添加值的最坚固的方法。我有点希望每个枚举值都是一个final子类,但我想你不能两者兼得,对吧? - GlenPeterson

12
当然,其他人会在这里提到许多优点。最重要的是,您可以快速编写 enums,它们可以执行许多操作,如实现SerializableComparableequals()toString()hashCode()等,这些都没有包含在您的枚举中。
但我可以向您展示一个enum严重缺陷(在我看来)。不仅您不能随意对其进行子类化,而且您也无法为其配备通用参数。之前您可以编写如下代码:
// A model class for SQL data types and their mapping to Java types
public class 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 {
        // [...]
    }
}

class Utility {

    // Enums equipped with generic types...
    public static <T> T doStuff(DataType<T> type) {
        return ...
    }
}

使用枚举是不可能实现这个的:

// This can't be done
public enum DataType<T> {

    // Neither can this...
    INT<Integer>("int", Integer.class), 
    INT4<Integer>("int4", Integer.class), 

    // [...]
}

1
你说得对。这个例子只是一个例子。我会尝试展现一个真实的问题。 - Lukas Eder
另一个小问题:为了使您的示例实际上是类型安全的,我建议在DataType中将parse()保持抽象,并在匿名内部类中实现它(每个值一个)。 - Joachim Sauer
1
你可以对枚举进行子类化,只需要将其作为匿名类型即可。其他任何方式都会破坏规范所做出的假设。 - josefx
1
@josefx,你能提供一个例子吗?如何对枚举进行子类化?Enum 无法被子类化,编译器会阻止这样做。而且具体的 enum 类型也无法被子类化,因为 enum 中的所有构造函数都必须是私有的... - Lukas Eder
1
@Lukas Eder 枚举类型 AnEnum{HELLO(){public void print(){System.out.println("Hello");}}, WORLD(){public void print(){System.out.println("World");}}; public abstract void print();} ____________ HELLO 和 WORLD 均作为 AnEnum 的子类实现。 - josefx
显示剩余4条评论

4

Now in JDK1.5+, the "official" way is obviously to use enum:

public enum Suit {
  CLUBS("clubs"), DIAMONDS("diamonds"), HEARTS("hearts"), SPADES("spades");

  private final String name;

  private Suit(String name) {
    this.name = name;
  }
}
实际上,更像是
 public enum Suit {
     CLUBS, DIAMONDS, HEARTS, SPADES;
 }

因为枚举类型已经提供了name()方法。此外,它们还提供了一个ordinal()方法(这使得像EnumSetEnumMap这样的高效数据结构成为可能),实现了Serializable接口,重写了toString方法,并提供了values()valueOf(String name)方法。它们可以在类型安全的switch语句中使用,并且是单例。


3
您的类型安全枚举实现有些过于简化。当您处理序列化时,它会变得更加复杂。
Java中的枚举解决了序列化/反序列化的问题。枚举被保证是唯一的,并且您可以使用"=="运算符进行比较。
请阅读《Effective Java 2nd Edition》中相应的章节(关于使用枚举替代单例,关于使用EnumSet等)。

使用枚举而不是单例模式?每个枚举项都是一个单例! - Sean Patrick Floyd
@Sean Patrick Floyd:是的,没错,但它不仅仅是那样(这就是重点)。 - sleske

2

EnumSetEnumMap是围绕枚举特性构建的自定义数据结构。它们具有方便的额外功能,并且非常快速。没有枚举,它们没有等同的替代品(至少没有与其相当优雅的使用方式,请参见注释)。


如果您将枚举替换为整数值而不是类型安全的枚举模式实现,则BitSet非常有效地替代了EnumSet - Joachim Sauer
@Joachim 已授权。但是API更加丑陋,而且EnumSet有很棒的工厂方法:copyOf()complementOf()等。 - Sean Patrick Floyd
我同意 EnumSet 更加好用,但我不同意 "没有相等的替代品"。 - Joachim Sauer
你可以在“类型安全的枚举类”中添加一个ordinal()方法,并基于它实现大部分功能。但是,使用enum会更好地书写代码,并且所有基础设施已经准备就绪。 - Paŭlo Ebermann

2
此外:
JDK5枚举类型可以在switch-case语句中轻松使用,并且有良好的IDE支持。
Suit suit = ...; 
switch (suit) { 
    case SPADES: System.out.println("Motorhead!"); break;
    default: System.out.println("Boring ..");
}

2

语法糖本身就值得一提 :-P 毕竟,这也是 for ( : ) 的作用。

但说真的,自动命名()和序数(),枚举它们,使用它们在switch()中,附加额外值给它们都是非常好的理由:它避免了大量样板代码。

传统的懒惰替代方案使用整数,不是类型安全的,而且更加有限。相比之下,枚举的一个缺点是它们不再轻量级。


实际上,我认为枚举类型相当轻量级。特别是,如果您在需要大量使用它们的情况下,可以使用EnumSet和EnumMap,它们都是轻量级的。 - sleske

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