【Java】枚举 Enum

Posted by 西维蜀黍 on 2019-03-27, Last Modified on 2025-04-23

背景

Java 语言中还没有引入枚举类型之前,表示枚举类型的常用模式是声明一组具有 int 常量。

我们通常利用 public final static 方法定义的代码如下,分别用 1 表示春天,2 表示夏天,3 表示秋天,4 表示冬天。

public class Season {
    public static final int SPRING = 1;
    public static final int SUMMER = 2;
    public static final int AUTUMN = 3;
    public static final int WINTER = 4;
}

这种方法称作 int 枚举模式。可这种模式有什么问题呢,我们都用了那么久了,应该没问题的。

通常我们写出来的代码都会考虑它的安全性易用性可读性

安全性

首先我们来考虑一下它的类型安全性。当然这种模式不是类型安全的。比如说我们设计一个函数,要求传入春夏秋冬的某个值。

但是使用 int 类型,我们无法保证传入的值为合法。

代码如下所示:

private String getChineseSeason(int season){
        StringBuffer result = new StringBuffer();
        switch(season){
            case Season.SPRING :
                result.append("春天");
                break;
            case Season.SUMMER :
                result.append("夏天");
                break;
            case Season.AUTUMN :
                result.append("秋天");
                break;
            case Season.WINTER :
                result.append("冬天");
                break;
            default :
                result.append("地球没有的季节");
                break;
        }
        return result.toString();
}

public void doSomething(){
    System.out.println(this.getChineseSeason(Season.SPRING));//这是正常的场景

    System.out.println(this.getChineseSeason(5));//这个却是不正常的场景,这就导致了类型不安全问题
}

易用性

程序 getChineseSeason(Season.SPRING) 是我们预期的使用方法,可 getChineseSeason(5) 显然就不是了,而且编译很通过,在运行时会出现什么情况,我们就不得而知了。这显然就不符合 Java 程序的类型安全。

可读性

接下来我们来考虑一下这种模式的可读性

使用枚举的大多数场合,我都需要方便地得到枚举类型的字符串表达式。如果将 int 枚举常量打印出来,我们所见到的就是一组数字,这是没什么太大的用处。

我们可能会想到使用 String 常量代替 int 常量。虽然它为这些常量提供了可打印的字符串,但是它会导致性能问题,因为它依赖于字符串的比较操作,所以这种模式也是我们不期望的。 从类型安全性程序可读性两方面考虑,intString 枚举模式的缺点就显露出来了。

幸运的是,从 Java1.5 发行版本开始,就提出了另一种可以替代的解决方案,可以避免 intString 枚举模式的缺点,并提供了许多额外的好处。那就是枚举类型(enum type)。

定义

枚举类型(enum type)是指由一组固定的常量组成合法的类型。Java 中由关键字 enum 来定义一个枚举类型。下面就是 Java 枚举类型的定义。

public enum Season {
    SPRING, SUMMER, AUTUMN, WINER;
}

特点

Java 定义枚举类型的语句很简约。它有以下特点:

  • 使用关键字 enum
  • 类型名称,比如这里的 Season
  • 一串允许的值,比如上面定义的春夏秋冬四季
  • 枚举可以单独定义在一个文件中,也可以嵌在其它 Java 类中

除了这样的基本要求外,用户还有一些其他选择

  • 枚举可以实现一个或多个接口(Interface)
  • 可以定义新的变量
  • 可以定义新的方法
  • 可以定义根据具体枚举值而相异的类

应用场景

以在背景中提到的类型安全为例,用枚举类型重写那段代码。代码如下:

public enum Season {
    SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4);

    private int code;
    private Season(int code){
        this.code = code;
    }

    public int getCode(){
        return code;
    }
}
public class UseSeason {
    /**
     * 将英文的季节转换成中文季节
     * @param season
     * @return
     */
    public String getChineseSeason(Season season){
        StringBuffer result = new StringBuffer();
        switch(season){
            case SPRING :
                result.append("[中文:春天,枚举常量:" + season.name() + ",数据:" + season.getCode() + "]");
                break;
            case AUTUMN :
                result.append("[中文:秋天,枚举常量:" + season.name() + ",数据:" + season.getCode() + "]");
                break;
            case SUMMER : 
                result.append("[中文:夏天,枚举常量:" + season.name() + ",数据:" + season.getCode() + "]");
                break;
            case WINTER :
                result.append("[中文:冬天,枚举常量:" + season.name() + ",数据:" + season.getCode() + "]");
                break;
            default :
                result.append("地球没有的季节 " + season.name());
                break;
        }
        return result.toString();
    }

    public void doSomething(){
        for(Season s : Season.values()){
            System.out.println(getChineseSeason(s));//这是正常的场景
        }
        //System.out.println(getChineseSeason(5));
        //此处已经是编译不通过了,这就保证了类型安全
    }

    public static void main(String[] arg){
        UseSeason useSeason = new UseSeason();
        useSeason.doSomething();
    }
}

输出

[中文:春天,枚举常量:SPRING,数据:1] [中文:夏天,枚举常量:SUMMER,数据:2] [中文:秋天,枚举常量:AUTUMN,数据:3] [中文:冬天,枚举常量:WINTER,数据:4]

这里有一个问题,为什么我要将域添加到枚举类型中呢?

目的是想将数据与它的常量关联起来。如 1 代表春天,2 代表夏天。

总结

那么什么时候应该使用枚举呢?

每当需要一组固定的常量的时候,如一周的天数、一年四季等。或者是在我们编译前就知道其包含的所有值的集合。Java 1.5 的枚举能满足绝大部分程序员的要求的,它的简明,易用的特点是很突出的。

源码分析

Enum 类是 Java.lang 包中一个类,他是 Java 语言中所有枚举类型的公共基类。

public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable

抽象类

首先,抽象类不能被实例化,所以我们在 Java 程序中不能使用 new 关键字来声明一个 Enum,如果想要定义可以使用这样的语法:

enum enumName{
    value1,value2
    method1(){}
    method2(){}
}

其次,看到抽象类,第一印象是肯定有类继承它。

至少我们应该是可以继承它的,所以:

public class testEnum extends Enum{
}
public class testEnum extends Enum<Enum<E>>{
}
public class testEnum<E> extends Enum<Enum<E>>{
}

尝试了以上三种方式之后,得出以下结论:Enum 类无法被继承

事实上,Enum 类中唯一的一个构造函数只能有编译器调用,因而这个

我们来反编译以下代码:

  enum Color {RED, BLUE, GREEN}

编译器将会把他转成如下内容:

public final class Color extends Enum<Color> {
  public static final Color[] values() { return (Color[])$VALUES.clone(); }
  public static Color valueOf(String name) { ... }

  private Color(String s, int i) { super(s, i); }

  public static final Color RED;
  public static final Color BLUE;
  public static final Color GREEN;

  private static final Color $VALUES[];

  static {
    RED = new Color("RED", 0);
    BLUE = new Color("BLUE", 1);
    GREEN = new Color("GREEN", 2);
    $VALUES = (new Color[] { RED, BLUE, GREEN });
  }
} 

短短的一行代码,被编译器处理过之后竟然变得这么多,看来,enmu 关键字是 Java 提供给我们的一个语法糖啊。

从反编译之后的代码中,我们发现,编译器不让我们继承 Enum,但是当我们使用 enum 关键字定义一个枚举的时候,他会帮我们在编译后默认继承 Java.lang.Enum 类,而不像其他的类一样默认继承 Object 类。

且采用 enum 声明后,该类会被编译器加上 final 声明,故该类是无法继承的。

实现 ComparableSerializable 接口

Enum 实现了 Serializable 接口,可以序列化。 Enum 实现了 Comparable 接口,可以进行比较,默认情况下,只有同类型的 enum 才进行比较(原因见后文),要实现不同类型的 enum 之间的比较,只能复写 compareTo 方法。

泛型:**<E extends Enum<E>>**

怎么理解 < E extends Enum>?

首先,这样写只是为了让 Java 的 API 更有弹性,他主要是限定形态参数实例化的对象,故要求只能是 Enum,这样才能对 compareTo 之类的方法所传入的参数进行形态检查。所以,我们完全可以不必去关心他为什么这么设计。

我们回到这个令人实在是无法理解的 < E extends Enum>

** 首先我们先来 “翻译” 一下这个 Enum<E extends Enum> 到底什么意思,** 然后再来解释为什么 Java 要这么用。

我们先看一个比较常见的泛型:List<String>。这个泛型的意思是,List 中存的都是 String 类型,告诉编译器要接受 String 类型,并且从 List 中取出内容的时候也自动帮我们转成 String 类型。 所以 Enum<E extends Enum<E>> 可以暂时理解为 Enum 里面的内容都是 E extends Enum<E> 类型。 这里的 E 我们就理解为枚举,extends 表示上界,比如: List<? extends Object>,List 中的内容可以是 Object 或者扩展自 Object 的类。这就是 extends 的含义。 所以,E extends Enum<E> 表示为一个继承了 Enum<E> 类型的枚举类型。 那么,Enum<E extends Enum<E>> 就不难理解了,就是一个 Enum 只接受一个 Enum 或者他的子类作为参数。相当于把一个子类或者自己当成参数,传入到自身,引起一些特别的语法效果。

为什么 Java 要这样定义 Enum

首先我们来科普一下 enum,

enum Color{
    RED,GREEN,YELLOW
}
enum Season{
    SPRING,SUMMER,WINTER
}
public class EnumTest{
    public static void main(String[] args) {
        System.out.println(Color.RED.ordinal());
        System.out.println(Season.SPRING.ordinal());
    }
}

代码中两处输出内容都是 0 ,因为枚举类型的默认的序号都是从零开始的。

要理解这个问题,首先我们来看一个 Enum 类中的方法(暂时忽略其他成员变量和方法):

public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
        private final int ordinal;

        public final int compareTo(E o) {
        Enum other = (Enum)o;
        Enum self = this;
        if (self.getClass() != other.getClass() && // optimization
            self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;
    }
}

首先我们认为 Enum 的定义中没有使用 Enum<E extends Enum<E>>,那么 compareTo 方法就要这样定义(因为没有使用泛型,所以就要使用 Object,这也是 Java 中很多方法常用的方式):

public final int compareTo(Object o) 

当我们调用 compareTo 方法的时候依然传入两个枚举类型,在 compareTo 方法的实现中,比较两个枚举的过程是先将参数转化成 Enum 类型,然后再比较他们的序号是否相等。那么我们这样比较:

Color.RED.compareTo(Color.RED);
Color.RED.compareTo(Season.SPRING);

如果在 compareTo 方法中不做任何处理的话,那么以上这段代码返回内容将都是 true(因为 Season.SPRING 的序号和 Color.RED 的序号都是 0 )。但是,很明显, Color.REDSeason.SPRING 并不相等。

但是 Java 使用 Enum<E extends Enum<E>> 声明 Enum,并且在 compareTo 的中使用 E 作为参数来避免了这种问题。 以上两个条件限制 Color.RED 只能和 Color 定义出来的枚举进行比较,当我们试图使用 Color.RED.compareTo(Season.SPRING); 这样的代码是,会报出这样的错误:

The method compareTo(Color) in the type Enum<Color> is not applicable for the arguments (Season)

他说明,compareTo 方法只接受 Enum<Color> 类型。

Java 为了限定形态参数实例化的对象,故要求只能是 Enum,这样才能对 compareTo 之类的方法所传入的参数进行形态检查。 因为 “红色” 只有和 “绿色” 比较才有意义,用 “红色” 和 “春天” 比较毫无意义,所以,Java 用这种方式一劳永逸的保证像 compareTo 这样的方法可以正常的使用而不用考虑类型。

PS:在 Java 中,其实也可以实现 “红色” 和 “春天” 比较,因为 Enum 实现了 Comparable 接口,可以重写 compareTo 方法来实现不同的 enum 之间的比较。

成员变量

在 Enum 中,有两个成员变量,一个是名字 (name),一个是序号 (ordinal)。 序号是一个枚举常量,表示在枚举中的位置,从 0 开始,依次递增。

/**
 * @author hollis
 */
private final String name
public final String name() {
    return name;
}
private final int ordinal;
public final int ordinal() {
    return ordinal;
}

构造函数

前面我们说过,Enum 是一个抽象类,不能被实例化,但是他也有构造函数,从前面我们反编译出来的代码中,我们也发现了 Enum 的构造函数,在 Enum 中只有一个保护类型的构造函数:

protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}

文章开头反编译的代码中 private Color(String s, int i) { super(s, i); } 中的 super(s, i); 就是调用 Enum 中的这个保护类型的构造函数来初始化 name 和 ordinal。

其他方法

Enum 当中有以下这么几个常用方法,调用方式就是使用 Color.RED.methodName(params...)的方式调用

public String toString() {
    return name;
}

public final boolean equals(Object other) {
    return this==other;
}

public final int hashCode() {
    return super.hashCode();
}

public final int compareTo(E o) {
    Enum other = (Enum)o;
    Enum self = this;
    if (self.getClass() != other.getClass() && // optimization
        self.getDeclaringClass() != other.getDeclaringClass())
        throw new ClassCastException();
    return self.ordinal - other.ordinal;
}

public final Class<E> getDeclaringClass() {
    Class clazz = getClass();
    Class zuper = clazz.getSuperclass();
    return (zuper == Enum.class) ? clazz : zuper;
}

public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {
    T result = enumType.enumConstantDirectory().get(name);
    if (result != null)
        return result;
    if (name == null)
        throw new NullPointerException("Name is null");
    throw new IllegalArgumentException(
        "No enum constant " + enumType.getCanonicalName() + "." + name);
}

方法内容都比较简单,平时能使用的就会也不是很多,这里就不详细介绍了。

用法

用法一 - 常量

public enum Color {  
  RED, GREEN, BLANK, YELLOW  
}  

用法二 - switch

enum Signal {  
    GREEN, YELLOW, RED  
}  
public class TrafficLight {  
    Signal color = Signal.RED;  
    public void change() {  
        switch (color) {  
        case RED:  
            color = Signal.GREEN;  
            break;  
        case YELLOW:  
            color = Signal.RED;  
            break;  
        case GREEN:  
            color = Signal.YELLOW;  
            break;  
        }  
    }  
}  

用法三 - 向枚举中添加新方法

public enum Color {  
    RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);  
    // 成员变量  
    private String name;  
    private int index;  
    // 构造方法  
    private Color(String name, int index) {  
        this.name = name;  
        this.index = index;  
    }  
    // 普通方法  
    public static String getName(int index) {  
        for (Color c : Color.values()) {  
            if (c.getIndex() == index) {  
                return c.name;  
            }  
        }  
        return null;  
    }  
    // get set 方法  
    public String getName() {  
        return name;  
    }  
    public void setName(String name) {  
        this.name = name;  
    }  
    public int getIndex() {  
        return index;  
    }  
    public void setIndex(int index) {  
        this.index = index;  
    }  
}  

用法四 - 覆盖枚举的方法

public enum Color {  
    RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);  
    // 成员变量  
    private String name;  
    private int index;  
    // 构造方法  
    private Color(String name, int index) {  
        this.name = name;  
        this.index = index;  
    }  
    //覆盖方法  
    @Override  
    public String toString() {  
        return this.index+"_"+this.name;  
    }  
}  

用法五 - 实现接口

public interface Behaviour {  
    void print();  
    String getInfo();  
}  
public enum Color implements Behaviour{  
    RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);  
    // 成员变量  
    private String name;  
    private int index;  
    // 构造方法  
    private Color(String name, int index) {  
        this.name = name;  
        this.index = index;  
    }  
//接口方法  
    @Override  
    public String getInfo() {  
        return this.name;  
    }  
    //接口方法  
    @Override  
    public void print() {  
        System.out.println(this.index+":"+this.name);  
    }  
}  

用法六 - 使用接口组织枚举

public interface Food {  
    enum Coffee implements Food{  
        BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO  
    }  
    enum Dessert implements Food{  
        FRUIT, CAKE, GELATO  
    }  
}

Reference