枚举系列文章目录:
【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 次 “构造方法被调用”。
自定义构造方法,注意以下两点:
- 自定义构造方法是否带参数,得看枚举实例,若枚举实例中带参数(见下文),则构造方法中得带参数;否则,就不能带参数
- 自定义构造方法前不要手动加
private
修饰,编译器会自动帮我们添加的
2. 枚举中的成员变量
之前有说过,枚举类和正常的类一样。那么,就可以有实例变量、实例方法、静态方法等等,只不过它的实例个数是有限的,不能再创建实例而已。
自定义成员变量
如:对于上述的枚举类 ColorEnum 而言,我不理解里面每一个实例的含义。例如:枚举实例 RED
,我怎么知道它表示的含义是 红色
呢?那么,我可以给它加一个描述,它就是表示红色。这样,别人一眼就明白了。
那么该怎么做呢?
可以给构造方法添加一个参数 desc,用来描述枚举实例的含义,在声明枚举实例时需要传入。即:
public enum ColorEnum {
RED("红色")
,
GREEN("绿色")
,
BLUE("蓝色")
;
private String desc;
ColorEnum(String desc) {
this.desc = desc;
}
}
注意:
- 此枚举类中只有一个带参的构造方法,所以声明枚举实例时,需要传入一个参数。也可声明多个构造方法(如:无参构造)
- 如果打算在枚举类中定义属性、方法,务必在声明完枚举实例后使用
;
分开。倘若在枚举实例前定义任何属性、方法,编译器都将会报错,无法编译通过 - 即使自定义了构造函数,我们也永远无法手动调用构造函数创建枚举实例,毕竟这事只能由编译器去做
再定义一个无参构造方法:
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()
方法可以改变值,这在语法上是合理的,但是,不要在项目中去使用它。
那么,这是为什么呢?
从两个方面来说吧:
- 枚举类本身的含义
- 在枚举类中使用了
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;
}
}
注意:
-
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());
...
}
}
上述代码为何会报空指针异常?
分析:
- 付款后,订单的状态为:
Order{sId='03ea74be-5509-4ec1-96e1-f60411b669a6', orderStatus=1002, orderTime=...}
。其orderStaus
为 1002。 - 继而又修改了订单的状态的 code 值。
orderStatusEnum.setCode(1001)
。这个操作会带来什么影响?它会将枚举实例HAS_PAID
(OrderStatusEnum orderStatusEnum = OrderStatusEnum.getOrderEnumByCode(payMoney.getOrderStatus());
)中的 code 值 由 1002 修改 1001。即:枚举类 OrderStatusEnum 中没有了 code 值为 1002 的枚举实例了 - 在调用方法
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;
...
}
由源码知:
-
java.lang.Enum
类是一个抽象类,实现了Comparable
、Serializable
接口,表明枚举类是可比较、可序列化的。 - 其有两个成员变量,分别为:
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
类中的方法作出一些说明:
-
java.lang.Enum
类中的方法大多是不能重写的,唯一能重写的方法就是toString()
方法 - 看看
equals()
方法,比较两个枚举类时,直接判断两个引用是否相等:return this == other
。这样写有问题吗?既然是源码,那肯定是没有问题的。那么,为什么可以这样写呢?之前有说过,枚举类使用了单例模式,保证了每个枚举实例的唯一性,且每个枚举实例都是静态常量。所以,直接比较它们的引用是没有问题的。类似于:String a = “hello”;String b = “hello”; a == b ? //true
-
compareTo()
方法是用来比较两个枚举实例的序数的。这两个枚举实例一定是同一个枚举类型的,否则,就会抛出异常!!
好了,关于枚举的知识就介绍到这了,下一篇介绍如何使用枚举吧。