枚举 Enum

235 阅读5分钟

枚举类型(Enum)是在 JDK 1.5 中引入的。引入枚举主要是为了提供一种类型安全的常量表示方式,替代传统的静态常量。通过枚举,可以定义一组固定的常量,并提供与这些常量相关的方法和字段。

枚举基本使用

▪ 定义枚举

使用 enum class 关键字可以定义一个枚举类。而枚举类内部第一行用逗号分隔的字符串为枚举常量,下面例子中,每个常量代表一周中的某一天。

package com.cango.enum

public enum class Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

▪ 使用枚举

可以像使用静态常量一样使用枚举即通过 Day.MONDAY 等语法访问枚举常量。

public static void main(String[] args) {
    Day day = Day.MONDAY;
    System.out.println("今天是: " + day);
    System.out.println("枚举类的类型是:" + Day.class);
    System.out.println("枚举类的常量和字符串一样吗?"+"MONDAY".equals(day));
}

输出结果如下:

今天是: MONDAY
枚举类的类型是:class com.cango.enums.Day
枚举类的常量和字符串一样吗?false

那枚举到底是个啥?

枚举的本质

使用 JAD 工具反编译枚举类,输出如下:

package com.cango.enums;

public final class Day extends Enum
{

    public static Day[] values()
    {
        return (Day[])$VALUES.clone();
    }
    public static Day valueOf(String name)
    {
        return (Day)Enum.valueOf(com/cango/enums/Day, name);
    }
    private Day(String s, int i)
    {
        super(s, i);
    }
    private static Day[] $values()
    {
        return (new Day[] {
            MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
        });
    }
    public static final Day MONDAY = new Day("MONDAY", 0);
    public static final Day TUESDAY = new Day("TUESDAY", 1);
    public static final Day WEDNESDAY = new Day("WEDNESDAY", 2);
    public static final Day THURSDAY = new Day("THURSDAY", 3);
    public static final Day FRIDAY = new Day("FRIDAY", 4);
    public static final Day SATURDAY = new Day("SATURDAY", 5);
    public static final Day SUNDAY = new Day("SUNDAY", 6);
    private static final Day $VALUES[] = $values();

}

可以看到,枚举类和枚举常量事实上都是继承自 java.lang.Enum 类,作为所有枚举类型的公共基类。每一个枚举类型都隐式地继承自 java.lang.Enum,也就是说它有的能力,枚举类也有,枚举常量也有,先看看它的结构和能做什么事情。

java.lang.Enum

package java.lang;
public abstract class Enum<E extends Enum<E>>
        implements Constable, Comparable<E>, Serializable

java.lang.Enum 是一个抽象类,成员属性如下:

⨳ String name:枚举常量的名称,在枚举声明中定义,如 "MONDAY";

⨳ int ordinal:枚举常量的序数,默认是该常量在枚举声明中的位置,从零开始,如 "MONDAY" 第一个声明,那它的 ordinal 就是 0;

知道了枚举的属性,再看枚举常量就不迷惑了,MONDAY 的本质是 {name="MONDAY",ordinal=0}, TUESDAY 的本质是 {name="TUESDAY",ordinal=1} ... 而枚举的构造方法就是为这两个属性赋值的:

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

既然枚举常量是 java.lang.Enum 类,和字符串类型进行比较会返回 false,这我能理解,那为啥打印出来的是字符串呢?打印默认调用的是 toString 方法,那就看它的 toString 方法呗:

public String toString() {
    return name;
}

一切尽在不言中....

那枚举和普通类相比还有什么特性呢?

⨳ 只有相同类型的枚举常量才能进行比较,而且比较的是 ordinal 不是 name

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

⨳ 不能被反序列化

@java.io.Serial
private void readObject(ObjectInputStream in) throws IOException,
    ClassNotFoundException {
    throw new InvalidObjectException("can't deserialize enum");
}

这一点在《单例模式》 一文中也讲到了, 这里节选如下:

在Java中,枚举类型的实例是在类加载时被创建的,并且在整个应用程序生命周期中只会有一个实例存在。当枚举类型的实例被序列化时,实际上是将枚举常量的名称写入到序列化输出流中,而不是将对象的状态写入。

在反序列化过程中,Java虚拟机会根据枚举常量的名称来查找对应的实例,而不是重新创建一个新的实例。这样就保证了在反序列化时获得的对象是相同的枚举实例,不会破坏枚举的单例性质。

枚举类

了解了 java.lang.Enum ,再看自定义的枚举类就很清晰了:

    public static final Day MONDAY = new Day("MONDAY", 0);
    public static final Day TUESDAY = new Day("TUESDAY", 1);
    public static final Day WEDNESDAY = new Day("WEDNESDAY", 2);
    public static final Day THURSDAY = new Day("THURSDAY", 3);
    public static final Day FRIDAY = new Day("FRIDAY", 4);
    public static final Day SATURDAY = new Day("SATURDAY", 5);
    public static final Day SUNDAY = new Day("SUNDAY", 6);

声明的枚举常量就是自定义枚举类型的一个静态常量对象而且,name 自己定义,ordinal 序号自动生成。

那既然枚举常量的类型就是自定义枚举类型,自定义枚举类型继承自 java.lang.Enum ,那是不是可以在自定义枚举的时候按需添加独属于自己的成员呢?

那是肯定的。

自定义枚举的方法

▪ 定义枚举

package com.cango.enums;

public enum TrafficLight {
    RED(30),
    GREEN(60),
    YELLOW(5);

    private int duration;

    TrafficLight(int duration) {
        this.duration = duration;
    }

    public int getDuration() {
        return duration;
    }
}

如上,定义了一个 TrafficLight 类型的枚举,TrafficLight作为枚举类,除了继承的 name 和 ordinal 属性外,还自定义了一个属性 duration,用于表示交通灯的持续时间。

重写的 TrafficLight 构造方法,看似没有为 name 和 ordinal 属性赋值,但实际上编译器会自动覆盖这个构造方法的,JAD反编译节选如下:

public final class TrafficLight extends Enum
{
    public static final TrafficLight RED = new TrafficLight("RED", 0, 30);
    public static final TrafficLight GREEN = new TrafficLight("GREEN", 1, 60);
    public static final TrafficLight YELLOW = new TrafficLight("YELLOW", 2, 5);
   
    private int duration;
    private TrafficLight(String s, int i, int duration)
    {
        super(s, i);
        this.duration = duration;
    }

    public int getDuration()
    {
        return duration;
    }
}

▪ 使用枚举

for (TrafficLight light : TrafficLight.values()) {
    System.out.println(light + ": " + light.getDuration() + " seconds");
}

输出结果如下:

RED: 30 seconds
GREEN: 60 seconds
YELLOW: 5 seconds

总结

枚举提供了一种简洁、易读的方式来定义一组相关的常量,避免了使用 public static final 定义常量的繁琐。

类型安全:枚举类型在编译时提供类型安全检查,防止非法的常量赋值。

可读性:枚举使代码更具可读性和可维护性,因为它们将一组相关的常量集合在一个类型中。

自动序列化:枚举类型的实例序列化和反序列化是自动处理的,减少了代码的复杂性。

总结来说,枚举提供了一组预定义的、相关联的常量,使用枚举会提高代码的可读性和类型安全性,常用于需要一组固定选项的场合,比如时间单元(天、小时、分钟)、工作日、季节等。