枚举系列文章目录:
【Enum】详解 Java 中的枚举类(上)【Enum】详解 Java 中的枚举类(下)【Enum】枚举在 Java 中的常用用法

在上一篇博文 【Enum】详解 Java 中的枚举类(上).,只是讲了下关于枚举类型的简单的定义。即:定义枚举类型时,只定义了枚举实例,并没有定义成员变量、方法。实际上,枚举类是可以做到这些的。也就是说,可以把枚举类当做一个常规类,除了不能使用继承(编译器会自动为我们继承 Enum 类)。接下来咱们看看枚举类的其它用法吧~~

1. 枚举类的构造方法

枚举类可以有构造方法,构造方法默认都是 private 修饰,而且只能是 private。因为枚举类的实例不能让外界来创建!那么,这是为什么呢?(为什么是 private 修饰?为什么枚举类的实例不能让外界来创建?)

在 JVM 中,为了保证每一个枚举类中的枚举实例的唯一性,是不会允许外部进行 new 的(防止用户生成实例,破坏唯一性),所以会把构造函数设计成private。即:枚举在 JDK 中被设计成了单例模式。

那么,什么时候会实例化枚举对象呢?

枚举类型的实例化都是在其加载的时候 JVM 帮我们完成的(你在枚举类中定义了多少个,就会实例化多少个)。JVM 规范明确规定:JVM 进行类加载时,会保证线程的安全性,即:在类加载实例化枚举类型时,会保证枚举实例的唯一性!!

自定义构造方法

自定义一个枚举类 ColorEnum,里面有三个枚举实例。如:

public enum ColorEnum {
    RED
    ,
    GREEN
    ,
    BLUE
    ;
	
	// 自定义构造方法
    ColorEnum() {
        System.out.println("构造方法被调用");
    }
}

枚举类 ColorEnum 中自定义了一个无参构造方法。创建枚举实例就等同于调用此类的参构造器。
所以,3 个实例,就会调用 3 次构造方法,就会打印 3 次 “构造方法被调用”。

自定义构造方法,注意以下两点:

  1. 自定义构造方法是否带参数,得看枚举实例,若枚举实例中带参数(见下文),则构造方法中得带参数;否则,就不能带参数
  2. 自定义构造方法前不要手动加 private 修饰,编译器会自动帮我们添加的

2. 枚举中的成员变量

之前有说过,枚举类和正常的类一样。那么,就可以有实例变量、实例方法、静态方法等等,只不过它的实例个数是有限的,不能再创建实例而已。

自定义成员变量

如:对于上述的枚举类 ColorEnum 而言,我不理解里面每一个实例的含义。例如:枚举实例 RED,我怎么知道它表示的含义是 红色 呢?那么,我可以给它加一个描述,它就是表示红色。这样,别人一眼就明白了。

那么该怎么做呢?

可以给构造方法添加一个参数 desc,用来描述枚举实例的含义,在声明枚举实例时需要传入。即:

public enum ColorEnum {
    RED("红色")
    ,
    GREEN("绿色")
    ,
    BLUE("蓝色")
    ;

    private String desc;

    ColorEnum(String desc) {
        this.desc = desc;
    }
}

注意:

  1. 此枚举类中只有一个带参的构造方法,所以声明枚举实例时,需要传入一个参数。也可声明多个构造方法(如:无参构造)
  2. 如果打算在枚举类中定义属性、方法,务必在声明完枚举实例后使用 ;分开。倘若在枚举实例前定义任何属性、方法,编译器都将会报错,无法编译通过
  3. 即使自定义了构造函数,我们也永远无法手动调用构造函数创建枚举实例,毕竟这事只能由编译器去做

再定义一个无参构造方法:

public enum ColorEnum {

	// 使用无参构造
    WHITE
    ,
    RED("红色")
    ,
    GREEN("绿色")
    ,
    BLUE("蓝色")
    ;

    private String desc;

	// 无参构造
    ColorEnum() {

    }

    ColorEnum(String desc) {
        this.desc = desc;
    }
}

定义一个无参构造方法后,就可以声明一个无参的枚举实例了。

3. 枚举中的成员方法

对于上述的枚举类 ColorEnum,我们可以很方便地知道每个枚举实例表示的含义。但是,有没有什么方法来获取到这个实例表示的含义呢?

当然可以!!想想类中是如何获取到对象的属性呢?就是通过属性的 getter() 方法嘛。

自定义成员方法

那我们就像类一样,来定义一个 getter() 方法吧。如:

public enum ColorEnum {

    WHITE
    ,
    RED("红色")
    ,
    GREEN("绿色")
    ,
    BLUE("蓝色")
    ;

    private String desc;

    ColorEnum() {

    }

    ColorEnum(String desc) {
        this.desc = desc;
    }

    public String getDesc() {
        return desc;
    }
}

接下来在代码中获取它:

public static void main(String[] args) {
    ColorEnum red = ColorEnum.RED;
    // 红色
    System.out.println(red.getDesc());
}

由结果来看,这种方式在枚举类中也是一样适用的。

想一想:既然枚举类中有获取值的方法 getter(),那么,它有没有设置值的方法 setter() 呢?



4. 枚举类的属性的 setter() 方法

来,咋们修改一下上面的代码看看吧:

public enum ColorEnum {

    RED("红色")
    ,
    GREEN("绿色")
    ,
    BLUE("蓝色")
    ;

    private String desc;

    ColorEnum(String desc) {
        this.desc = desc;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

上述代码中,在原有的代码上添加了 setter() 方法。

接下来在 main() 方法中调用:

public static void main(String[] args) {
    ColorEnum red = ColorEnum.RED;
	// 红色
    System.out.println(red.getDesc());

    red.setDesc("黑色");
    // 黑色
    System.out.println(red.getDesc());
}

嘿嘿,setter() 方法也生效了喔。

【注意】:虽然枚举类中的 setter() 方法可以改变值,这在语法上是合理的,但是,不要在项目中去使用它。

那么,这是为什么呢?

从两个方面来说吧:

  1. 枚举类本身的含义
  2. 在枚举类中使用了 setter() 方法可能会导致的问题

枚举类本身的含义

枚举只是一种语法糖,最终会被编译器生成类,而枚举实例会变成静态常量。因此,从某种意义上说,jdk1.5 引入的枚举类型是枚举常量类的代码封装。当用setter() 方法进行修改值的时候,实际上是修改的一个内存中的静态变量的值,这个值原本的意义就被修改了

在枚举类中使用了 setter() 方法可能会导致的问题

这个可能会导致什么问题呢?(setter() 方法本身不会有问题)

大家可以看看如下场景:

有一个订单枚举类,枚举类中封装了订单的状态。在正常的业务逻辑中,会根据订单的状态做出相应的处理。

代码如下:

订单类 Order:

public class Order {
    // 主键
    private String sId;
    // 订单状态
    private Integer orderStatus;
    // 下单时间
    private Date orderTime;
	
	// getter/setter/toString
	...
}

订单状态枚举类 OrderStatusEnum:

public enum OrderStatusEnum {

    WAIT_PAID(1001, "待付款")
    ,
    HAS_PAID(1002, "已付款")
    ,
    DELIEVER(1003, "派送中")
    ;

    // 状态码
    private Integer code;
    // 状态信息
    private String msg;

    OrderStatusEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
	
	// 根据 code 获取 OrderStatusEnum
    public static OrderStatusEnum getOrderEnumByCode(Integer code) {
        OrderStatusEnum[] values = OrderStatusEnum.values();
        for (OrderStatusEnum value : values) {
            if (value.getCode().equals(code)) {
                return value;
            }
        }
        return null;
    }

    // getter/setter/toString()
}

订单业务逻辑类:

public class OrderServiceImpl {

    // 下单
    public Order saveOrder(Order order) {
        Order o = new Order();

        o.setOrderTime(new Date());
        if (null == o.getsId()) {
            o.setsId(UUID.randomUUID().toString());
        }
        // 下完单后,订单状态为“待付款”
        o.setOrderStatus(OrderStatusEnum.WAIT_PAID.getCode());
        return o;
    }

    // 付款
    public Order payMoney(Order order) {
        // 正常情况下,通过 orderId 去查询
        Order o = new Order();

        o.setsId(order.getsId());
        // 只有是待付款状态,才会更新状态为已付款
        if (OrderStatusEnum.WAIT_PAID.getCode().equals(order.getOrderStatus())) {
            o.setOrderStatus(OrderStatusEnum.HAS_PAID.getCode());
        }
        o.setOrderTime(order.getOrderTime());
        return o;
    }

    // 派送
    public Order deliever(Order order) {
        // 正常情况下,通过 orderId 去查询
        Order o = new Order();

        o.setsId(order.getsId());
        // 只有是待付款状态,才会更新状态为已付款
        if (OrderStatusEnum.HAS_PAID.getCode().equals(order.getOrderStatus())) {
            o.setOrderStatus(OrderStatusEnum.DELIEVER.getCode());
        }
        o.setOrderTime(order.getOrderTime());
        return o;
    }
}

注意:

  1. payMoney()deliever() 方法中,修改订单状态时,先提前对订单状态进行了判断,只有订单状态符合,才会进行相应的业务逻辑处理。如:只有订单状态是“待付款”时(对应的 code 值 为 1001),调用 payMoney() 方法,修改订单状态才会成功;如果调用 deliever() 方法,会修改订单状态失败

测试:

public class OrderEnumTest {

    public static void main(String[] args) {

        OrderServiceImpl orderService = new OrderServiceImpl();

        // 刚下单
        Order order = orderService.saveOrder(new Order());

        // 付款后
        Order payMoney = orderService.payMoney(order);
        System.out.println(payMoney);
        System.out.println("订单状态:" + OrderStatusEnum.getOrderEnumByCode(payMoney.getOrderStatus()).getMsg());

        // 派送中
        Order deliever = orderService.deliever(payMoney);
        System.out.println(deliever);
    }
}

正常情况下,当成功调用完 orderService.deliever(payMoney) 方法后,你的订单状态是 “派送中”的。

但由于订单状态是封装在枚举类中,而枚举类中又是可以对订单状态的 code 值 进行修改。所以,如果在某处的业务逻辑中对订单的状态 code 值进行了修改。即:

// 某个业务逻辑修改了订单的状态的 code 值
OrderStatusEnum orderStatusEnum = OrderStatusEnum.getOrderEnumByCode(payMoney.getOrderStatus());
orderStatusEnum.setCode(1001);

上述操作,会将枚举类 OrderStatusEnum 中的枚举实例 HAS_PAID 的 code 值修改为 1001

完整代码如下:

public class OrderEnumTest {

    public static void main(String[] args) {

        OrderServiceImpl orderService = new OrderServiceImpl();

        // 刚下单
        Order order = orderService.saveOrder(new Order());

        // 付款后
        Order payMoney = orderService.payMoney(order);
        System.out.println(payMoney);
        System.out.println("订单状态:" + OrderStatusEnum.getOrderEnumByCode(payMoney.getOrderStatus()).getMsg());

        // 某个业务逻辑修改了订单的状态的 code 值
        // 此时的枚举实例是 HAS_PAID
        OrderStatusEnum orderStatusEnum = OrderStatusEnum.getOrderEnumByCode(payMoney.getOrderStatus());
        orderStatusEnum.setCode(1001);

        System.out.println(payMoney);
        
        // 此处报空指针异常
        System.out.println("订单状态:" + OrderStatusEnum.getOrderEnumByCode(payMoney.getOrderStatus()).getMsg());

        ...
    }
}

上述代码为何会报空指针异常?

分析:

  1. 付款后,订单的状态为:Order{sId='03ea74be-5509-4ec1-96e1-f60411b669a6', orderStatus=1002, orderTime=...}。其 orderStaus 为 1002。
  2. 继而又修改了订单的状态的 code 值。orderStatusEnum.setCode(1001)。这个操作会带来什么影响?它会将枚举实例 HAS_PAIDOrderStatusEnum orderStatusEnum = OrderStatusEnum.getOrderEnumByCode(payMoney.getOrderStatus());)中的 code 值 由 1002 修改 1001。即:枚举类 OrderStatusEnum 中没有了 code 值为 1002 的枚举实例了
  3. 在调用方法 OrderStatusEnum.getOrderEnumByCode(payMoney.getOrderStatus()) 时,由于 payMoney.getOrderStatus() 的值为 1002,所以,在枚举类 OrderStatusEnum 中获取不到相应的枚举实例,所以,会返回为 null。在调用 getMsg() 方法时,必然会 NPE。

产生 NPE 的根本原因在于:

你对枚举类 OrderStatusEnum 中的枚举实例的 code 值进行了修改:

orderStatusEnum.setCode(1001)

然后,你又在代码中,又根据 code 去进行判断,然后获取对应的枚举实例:

public static OrderStatusEnum getOrderEnumByCode(Integer code) {
    OrderStatusEnum[] values = OrderStatusEnum.values();
    for (OrderStatusEnum value : values) {
    	// 根据 code 值进行判断
        if (value.getCode().equals(code)) {
            return value;
        }
    }
    return null;
}

最终,代码肯定会出问题。

以上的场景只是针对枚举类中的 setter() 方法可能导致的问题哈。所以,尽量不要在开发中去使用枚举类中实例成员的 setter() 方法。因为,你根本不知道可能会引发什么样的问题!!

5. java.lang.Enum 类

根据前面的知识,我们知道了 Java 中的枚举类是继承 java.lang.Enum 类。但是,仅仅只是知道了它的存在,并没有真正地去了解它。那么,接下来就简单地介绍下它吧~~

查看 java.lang.Enum 类的源码:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
	
	// 枚举常量名称
	private final String name;
	
	// 枚举常量序数,从 0 开始
	private final int ordinal;
	...
}

由源码知:

  1. java.lang.Enum 类是一个抽象类,实现了 ComparableSerializable 接口,表明枚举类是可比较、可序列化的。
  2. 其有两个成员变量,分别为:name 枚举常量名称、ordinal 枚举常量序数


java.lang.Enum 类中的常见方法

// 返回此枚举常量的名称
public final String name() {
    return name;
}

// 返回枚举常量的序数
public final int ordinal() {
    return ordinal;
}

// 比较两个枚举是否相等
public final boolean equals(Object other) {
    return this == other;
}

// 根据两个枚举类型的序数进行比较
public final int compareTo(E o) {
    Enum<?> other = (Enum<?>)o;
    Enum<E> self = this;
    if (self.getClass() != other.getClass() && // optimization
        self.getDeclaringClass() != other.getDeclaringClass())
        throw new ClassCastException();
    return self.ordinal - other.ordinal;
}

// 根据枚举类型、枚举名称返回一个枚举常量
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);
}

这里对 java.lang.Enum 类中的方法作出一些说明:

  1. java.lang.Enum 类中的方法大多是不能重写的,唯一能重写的方法就是 toString() 方法
  2. 看看 equals() 方法,比较两个枚举类时,直接判断两个引用是否相等:return this == other。这样写有问题吗?既然是源码,那肯定是没有问题的。那么,为什么可以这样写呢?之前有说过,枚举类使用了单例模式,保证了每个枚举实例的唯一性,且每个枚举实例都是静态常量。所以,直接比较它们的引用是没有问题的。类似于:String a = “hello”;String b = “hello”; a == b ? //true
  3. compareTo() 方法是用来比较两个枚举实例的序数的。这两个枚举实例一定是同一个枚举类型的,否则,就会抛出异常!!

好了,关于枚举的知识就介绍到这了,下一篇介绍如何使用枚举吧。