简介:
枚举是 JDK 1.5 新增的数据类型,使用枚举我们可以很好的描述一些特定的业务场景,比如一年中的春、夏、秋、冬,还有每周的周一到周天,还有各种颜色,以及可以用它来描述一些状态信息,比如错误码等。
枚举类型不止存在在 Java 语言中,在其它语言中也都能找到它的身影,例如 C# 和 Python 等,但我发现在实际的项目中使用枚举的人很少,所以本文就来聊一聊枚举的相关内容,好让朋友们对枚举有一个大概的印象,这样在编程时起码还能想到有“枚举”这样一个类型。
如果一个类的对象个数是有限的而且是不变的,我们通常将这样的类设计成枚举类。
enum关键字与class和interface地位相同,其一样有成员变量、方法、可以实现一个或多个接口,也可以有构造器。
目录:
- 抽象类Enum类源码
- 枚举类
- 定义及实现原理
- 基本方法的使用
- 添加方法与自定义构造函数
- 覆盖枚举方法
- 实现接口
- 包含抽象方法的枚举类
- 枚举类的用途
- 作为常量
- switch
- 在接口中组织枚举类
- 使用枚举集合
- 枚举类和普通类的区别
- 枚举类和常量类的区别(假如不使用枚举)
- 使用枚举的注意事项
- 枚举类为什么是线程安全的
- 枚举类的在java中使用场景
- java.Month
- 单例 深入理解Java枚举类型(enum) - 沾青先生 - 博客园
- EnumMap
- EnumSet
- spring-web.jar下的org.springframework.http.HttpStatus.class
注意其重写了toString方法,为什么这样做要了解
- 在项目中的使用:实战
- 优秀博客
- 可参考:关于枚举类你可能不知道的事 - 程序员自由之路 - 博客园
- 枚举类的定义
- 枚举类的底层实现
- 枚举类的序列化实现
- 用枚举实现单列
- 枚举实例的创建过程是线程安全的
- 恕我直言,我怀疑你没怎么用过枚举
- 为什么需要枚举
- 认识枚举
- 自定义扩充枚举
- 枚举 + 接口 = ?
- 枚举与设计模式
- 专门用于枚举的集合类
- 可参考:关于枚举类你可能不知道的事 - 程序员自由之路 - 博客园
一.抽象类Enum类源码
定义的方法:
- 实现了Comparable接口,所有其子类可以使用compareTo(E e)方法。例如:enum1.compareTo(enum2),返回值为enum1.ordinal()-enum2.ordinal()
- name():返回此枚举实例的名称
- ordinal():该方法获取的是枚举变量在枚举类中声明的顺序,下标从0开始
- values():返回所有枚举实例
- valueOf():一个静态方法,用于返回指定枚举类中指定名称的枚举值
//实现了Comparable
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
private final String name; //枚举字符串名称
public final String name() {
return name;
}
private final int ordinal;//枚举顺序值
public final int ordinal() {
return ordinal;
}
//枚举的构造方法,只能由编译器调用
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
public String toString() {
return name;
}
public final boolean equals(Object other) {
return this==other;
}
//比较的是ordinal值
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;//根据ordinal值比较大小
}
@SuppressWarnings("unchecked")
public final Class<E> getDeclaringClass() {
//获取class对象引用,getClass()是Object的方法
Class<?> clazz = getClass();
//获取父类Class对象引用
Class<?> zuper = clazz.getSuperclass();
return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
}
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
//enumType.enumConstantDirectory()获取到的是一个map集合,key值就是name值,value则是枚举变量值
//enumConstantDirectory是class对象内部的方法,根据class对象获取一个map集合的值
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);
}
//.....省略其他没用的方法
}
二.枚举类
2.1 枚举类的定义及实现原理
package test;
//枚举类型,使用关键字enum
enum Color {
// 会产生如下四个实例
RED,BLUE,YELLOW,BLACK,GREEN;
}
枚举类Color经过Java编译器编译后,如果我们反编译class文件可以看到如下代码:
public final class test.Color extends java.lang.Enum<test.Color> {
public static final test.Color RED;
public static final test.Color BLUE;
public static final test.Color YELLOW;
public static final test.Color BLACK;
public static final test.Color GREEN;
public static test.Color[] values();
public static test.Color valueOf(java.lang.String);
static {
RED = new test.Color("RED", 0);
BLUE = new test.Color("BLUE", 1);
YELLOW = new test.Color("YELLOW", 2);
BLACK = new test.Color("WINTER", 3);
GREEN = new test.Color("GREEN", 4);
ENUM$VALUES = (new test.Color[] {
RED, BLUE, YELLOW, BLACK, GREEN
})
};
}
结论:
-
枚举类的父类是java.lang.Enum类
-
非抽象枚举类默认使用final修饰,不能存在子类;抽象的枚举类,系统默认使用abstract修饰,而不用final修饰
-
枚举类的构造器只能使用private修饰符,默认就是private
-
枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远不能产生实例,列出的实例,系统会自动添加public static final修饰
枚举值就代表可能会产生的实例
2.2 基本方法的使用
package test;
import java.util.Arrays;
public class EnumTest {
public static void main(String[] args) {
Color black = Color.BLACK;
Color red = Color.RED;
// values()
System.out.println(Arrays.toString(Color.values())); // [RED, BLUE, YELLOW, BLACK, GREEN]
// name()
System.out.println(black.name());
// toString()
System.out.println(black.toString());
// valueOf()
System.out.println(Color.valueOf("RED"));
// compareTo()
System.out.println(black.compareTo(red)); // 3-0=3
// ordinal() 返回枚举值在枚举类中的索引值(从0开始)
System.out.println(black.ordinal()); // 3
}
}
2.3 枚举中增加方法
我们可以在枚举中增加一些方法,让枚举具备更多的特性,实现代码如下:
package test;
public enum Color {
// 实例对象
RED("红色", 1),BLUE("蓝色", 2),YELLOW("黄色", 3),BLACK("黑色", 4),GREEN("绿色", 5);
// 成员变量
private String name;
private int code;
// 私有的构造方法
private Color(String name,int code) {
this.name = name;
this.code = code;
}
// 增加的方法
public String getName() {
return name;
}
// 增加的方法
public int getCode() {
return code;
}
// 增加的方法
public static Color getColor(int code) {
for(Color color : Color.values()) {
if(color.code == code) {
return color;
}
}
return null;
}
}
public class EnumTest {
public static void main(String[] args) {
Color black = Color.BLACK;
System.out.println("name;"+black.getName()+";"+"code:"+black.getCode()); // name;黑色;code:4
Color color = Color.getColor(3);
System.out.println(color.getName()); // 黄色
}
}
2.4 覆盖枚举方法
我们可以覆盖一些枚举中的方法用于实现自己的业务,比如我们可以覆盖 toString()
方法,实现代码如下:
public class EnumTest {
public static void main(String[] args) {
ColorEnum colorEnum = ColorEnum.RED;
System.out.println(colorEnum.toString());
}
}
public enum Color {
// 实例对象
RED("红色", 1),BLUE("蓝色", 2),YELLOW("黄色", 3),BLACK("黑色", 4),GREEN("绿色", 5);
// 成员变量
private String name;
private int code;
// 私有的构造方法
private Color(String name,int code) {
this.name = name;
this.code = code;
}
// 覆盖方法
@Override
public String toString() {
return "name:"+this.name+";code:"+this.code;
}
}
public class EnumTest {
public static void main(String[] args) {
Color black = Color.BLACK;
System.out.println(black.toString());
}
}
以上程序的执行结果为:
name:黑色;code:4
2.5 实现接口
枚举类可以用来实现接口,但不能用于继承类,因为枚举默认继承了 java.lang.Enum
类,在 Java 语言中允许实现多接口,但不能继承多个父类,实现代码如下:
package test;
public interface Shape {
String draw(String size);
}
public enum Color implements Shape{
// 实例对象
RED("红色", 1),BLUE("蓝色", 2),YELLOW("黄色", 3),BLACK("黑色", 4),GREEN("绿色", 5);
// 成员变量
private String name;
private int code;
// 私有的构造方法
private Color(String name,int code) {
this.name = name;
this.code = code;
}
@Override
public String draw(String size) {
return size+this.name;
}
}
public class EnumTest {
public static void main(String[] args) {
Color black = Color.BLACK;
System.out.println(black.draw("big"));
}
}
以上程序的执行结果为:
big黑色
2.6 包含抽象方法的枚举类
枚举类里定义抽象方法时不能使用abstract关键字将枚举类定义成抽象类(因为系统自动会添加abastract),但因为枚举类需要显式创建枚举值,而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则出现编译错误
package test;
public enum Color{
// 实例对象
RED("红色", 1) {
@Override
public String getNameAndCode() {
return "红色";
}
},BLUE("蓝色", 2) {
@Override
public String getNameAndCode() {
return "蓝色";
}
},YELLOW("黄色", 3) {
@Override
public String getNameAndCode() {
return "黄色";
}
},BLACK("黑色", 4) {
@Override
public String getNameAndCode() {
return "黑色";
}
},GREEN("绿色", 5) {
@Override
public String getNameAndCode() {
return "绿色";
}
};
// 成员变量
private String name;
private int code;
// 私有的构造方法
private Color(String name,int code) {
this.name = name;
this.code = code;
}
public abstract String getNameAndCode();
}
public class EnumTest {
public static void main(String[] args) {
Color black = Color.BLACK;
System.out.println(black.getNameAndCode()); // 黑色
}
}
三.枚举类的用途
用法一:常量
在 JDK 1.5 之前,我们定义常量都是 public static final...
,但有了枚举,我们就可以把这些常量定义成一个枚举类了,实现代码如下:
package test;
//枚举类型,使用关键字enum
enum Color {
// 会产生如下四个实例
RED,BLUE,YELLOW,BLACK,GREEN;
}
用法二:switch
将枚举用在 switch 判断中,使得代码可读性更高了,实现代码如下:
package test;
public class EnumTest {
public static void main(String[] args) {
Color color = Color.BLACK;
System.out.println(getColor(color)); // 黑色
}
public static String getColor(Color color) {
switch(color) {
case RED: {
return "红色";
}
case BLUE: {
return "蓝色";
}
case YELLOW: {
return "黄色";
}
case BLACK: {
return "黑色";
}
case GREEN: {
return "绿色";
}
}
return null;
}
}
用法三:在接口中组织枚举类
我们可以在一个接口中创建多个枚举类,用它可以很好的实现“多态”,也就是说我们可以将拥有相同特性,但又有细微实现差别的枚举类聚集在一个接口中,实现代码如下:
public class TestEnum {
public static void main(String[] args) {
// 赋值第一个枚举类
ColorInterface colorEnum = ColorInterface.ColorEnum.RED;
System.out.println(colorEnum);
// 赋值第二个枚举类
colorEnum = ColorInterface.NewColorEnum.NEW_RED;
System.out.println(colorEnum);
}
}
package test;
public interface ColorInterface {
enum ColorEnum implements ColorInterface {
GREEN, YELLOW, RED
}
enum NewColorEnum implements ColorInterface {
NEW_GREEN, NEW_YELLOW, NEW_RED
}
}
以上程序的执行结果为:
RED
NEW_RED
用法四:使用枚举集合
在 Java 语言中和枚举类相关的,还有两个枚举集合类 java.util.EnumSet
和 java.util.EnumMap
,使用它们可以实现更多的功能。
使用 EnumSet
可以保证元素不重复,并且能获取指定范围内的元素,示例代码如下:
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
public class EnumTest {
public static void main(String[] args) {
List<ColorEnum> list = new ArrayList<ColorEnum>();
list.add(ColorEnum.RED);
list.add(ColorEnum.RED); // 重复元素
list.add(ColorEnum.YELLOW);
list.add(ColorEnum.GREEN);
// 去掉重复数据
EnumSet<ColorEnum> enumSet = EnumSet.copyOf(list);
System.out.println("去重:" + enumSet);
// 获取指定范围的枚举(获取所有的失败状态)
EnumSet<ErrorCodeEnum> errorCodeEnums = EnumSet.range(ErrorCodeEnum.ERROR, ErrorCodeEnum.UNKNOWN_ERROR);
System.out.println("所有失败状态:" + errorCodeEnums);
}
}
enum ColorEnum {
RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLOW("黄色", 4);
private String name;
private int index;
private ColorEnum(String name, int index) {
this.name = name;
this.index = index;
}
}
enum ErrorCodeEnum {
SUCCESS(1000, "success"),
ERROR(2001, "parameter error"),
SYS_ERROR(2002, "system error"),
NAMESPACE_NOT_FOUND(2003, "namespace not found"),
NODE_NOT_EXIST(3002, "node not exist"),
NODE_ALREADY_EXIST(3003, "node already exist"),
UNKNOWN_ERROR(9999, "unknown error");
private int code;
private String msg;
ErrorCodeEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int code() {
return code;
}
public String msg() {
return msg;
}
}
以上程序的执行结果为:
去重:[RED, GREEN, YELLOW]
所有失败状态:[ERROR, SYS_ERROR, NAMESPACE_NOT_FOUND, NODE_NOT_EXIST, NODE_ALREADY_EXIST, UNKNOWN_ERROR]
EnumMap
与 HashMap
类似,不过它是一个专门为枚举设计的 Map
集合,相比 HashMap
来说它的性能更高,因为它内部放弃使用链表和红黑树的结构,采用数组作为数据存储的结构。
EnumMap
基本使用示例如下:
import java.util.EnumMap;
public class EnumTest {
public static void main(String[] args) {
EnumMap<ColorEnum, String> enumMap = new EnumMap<>(ColorEnum.class);
enumMap.put(ColorEnum.RED, "红色");
enumMap.put(ColorEnum.GREEN, "绿色");
enumMap.put(ColorEnum.BLANK, "白色");
enumMap.put(ColorEnum.YELLOW, "黄色");
System.out.println(ColorEnum.RED + ":" + enumMap.get(ColorEnum.RED));
}
}
enum ColorEnum {
RED, GREEN, BLANK, YELLOW;
}
以上程序的执行结果为:
RED:红色
四.枚举类和普通类的区别
枚举类说了这么多,再回头看看和普通类的区别
枚举是java 5新增了一个menu关键字(它与class、interface关键字的地位相同),用于定义枚举,枚举是一种特殊的类,它一样有自己的Fileld、方法,可以实现一个或多个接口,也可以定义自己的构造器。一个java源文件中最多只能定义一个public访问权限的枚举类,且该java源文件也必须和枚举类的类名相同。但枚举毕竟不是普通的java类,它与普通类有如下简单的区别:
-
枚举类可以实现一个或多个接口,使用menu定义的枚举直接继承了java.long.Enum类,而不是继承Object类。其中java.long.Enum类实现了java.long.Serializable和java.long.Comparable两个接口。
-
使用enum定义、非抽象的枚举默认修饰符为final,因此枚举不能派生子类。
-
枚举的构造器只能使用private访问控制符,如果省略了枚举的访问修饰符其默认为private修饰;如果加强制定访问修饰符则只能使用private。
-
枚举的所有实例必须在枚举的第一行显示列出,否则这个枚举永远都不能生产实例,列出这些实例时系统会自动添加public static final修饰,无需程序员显式添加
-
所有的枚举类都提供了一个values方法,该方法可以很方便的遍历所有的枚举值
五.枚举类和常量类的区别
假如有一笔业务需要审核,审核状态分:未审核,审核中,审核通过,审核不通过。我们在程序里是否可以直接这么写:
if(state==1){//1代表未操作
//操作
}else{
//......
}
将状态标识直接写在代码里面(硬编码),只图一时方便,却是后患无穷,如果有一天你需要修改状态标识,用0代表未审核而不是1,你不得不将所有与该标识相关的代码都找出来一个个改,另外,在编码过程中,标识输入错误的概率是比较高的,一不小心把0输入成了10,虽然不会提示任何编译错误,但运行结果将是出乎人的意料的。
于是我们很快想到可以用常量代替:
publicstaticfinalintUNAUDIT = 0;
相关判断代码则是:
if(state==CONSTANT.UNAUDIT){
//操作
}else{
//......
}
这段代码比硬编码更加健壮容易维护,但是仍然有不足之处。
1、UNAUDIT是编译期常量,如果其值被改变,那么使用方需要重新编译。
2、没有简便的方法获取标识代表的字符串描述。
于是我们用枚举类来代替常量。
publicenum AuditState {
UNAUDIT(1),
AUDITING(2),
AUDIT_SUCCESS(3),
AUDIT_FAIL(4);
privatefinalint statenum;
AuditState(int statenum){
this.statenum = statenum;
}
publicint getStatenum() {
return statenum;
}
}
调用如下:
if (state == AuditState.UNAUDIT.getStatenum()) {
//AuditState.UNAUDIT.toString()获取字符串描述
System.out.println(AuditState.UNAUDIT.toString() + "标识是 "
+ AuditState.UNAUDIT.getStatenum());
} else {
//......
}
枚举类还有更加强大的功能,如添加字段,方法,还可以对进行遍历访问
六.使用注意事项
阿里《Java开发手册》对枚举的相关规定如下,我们在使用时需要稍微注意一下。
【强制】所有的枚举类型字段必须要有注释,说明每个数据项的用途。
【参考】枚举类名带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。说明:枚举其实就是特殊的常量类,且构造方法被默认强制是私有。正例:枚举名字为 ProcessStatusEnum 的成员名称:SUCCESS / UNKNOWN_REASON。
枚举使用场景
枚举的常见使用场景是单例,它的完整实现代码如下:
public class Singleton {
// 枚举类型是线程安全的,并且只会装载一次
private enum SingletonEnum {
INSTANCE;
// 声明单例对象
private final Singleton instance;
// 实例化
SingletonEnum() {
instance = new Singleton();
}
private Singleton getInstance() {
return instance;
}
}
// 获取实例(单例对象)
public static Singleton getInstance() {
return SingletonEnum.INSTANCE.getInstance();
}
private Singleton() {
}
// 类方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}
class SingletonTest {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
singleton.sayHi();
}
}
因为枚举只会在类加载时装载一次,所以它是线程安全的,这也是《Effective Java》作者极力推荐使用枚举来实现单例的主要原因。
七.枚举为什么是线程安全的?
这一点要从枚举最终生成的字节码说起,首先我们先来定义一个简单的枚举类:
public enum ColorEnumTest {
RED, GREEN, BLANK, YELLOW;
}
然后我们再将上面的那段代码编译为字节码,具体内容如下:
public final class ColorEnumTest extends java.lang.Enum<ColorEnumTest> {
public static final ColorEnumTest RED;
public static final ColorEnumTest GREEN;
public static final ColorEnumTest BLANK;
public static final ColorEnumTest YELLOW;
public static ColorEnumTest[] values();
public static ColorEnumTest valueOf(java.lang.String);
static {};
}
从上述结果可以看出枚举类最终会被编译为被 final
修饰的普通类,它的所有属性也都会被 static
和 final
关键字修饰,所以枚举类在项目启动时就会被 JVM 加载并初始化,而这个执行过程是线程安全的,所以枚举类也是线程安全的类。
小贴士:代码反编译的过程是先用 javac 命令将 java 代码编译字节码(.class),再使用 javap 命令查看编译的字节码。
枚举比较小技巧
我们在枚举比较时使用 == 就够了,因为枚举类是在程序加载时就创建了(它并不是 new
出来的),并且枚举类不允许在外部直接使用 new
关键字来创建枚举实例,所以我们在使用枚举类时本质上只有一个对象,因此在枚举比较时使用 == 就够了。
并且我们在查看枚举的 equlas()
源码会发现,它的内部其实还是直接调用了 == 方法,源码如下:
public final boolean equals(Object other) {
return this==other;
}
八.枚举类的在java中使用场景
1.枚举enum - java.Month
九.实战:
public class OrderConstant {
/**
*
* @Title: OrderConstant.java
* @Package com.boot.common.base
* @ClassName: OrderState
* @Description: 订单状态
* @date 2019年3月12日 下午1:57:04
*/
public static enum OrderState {
_default(""),waitPay("待付款"),waitSend("待发货"),waitSelfTaking("上门自取,待自取"), offlineWaitConfirm("待确认"),
offlineConfirmed("已确认待发货"), send("已发货"), refunded("已退款"), complete("已完成"),receiveEnd("礼物已领完"),transactionClose("交易关闭"),
waitReceive("待领取"), waitWriteOff("未核销"), writeOff("已核销"), warehouse("已收仓"), receiveSuccess("领取成功"), warehouseSuccess("收仓成功"),isCancel("已取消"),coinsExchange("已兑换");
private String strDes;// 订单状态描述
private OrderState(String strDes) {
this.strDes = strDes;
}
public String getStrDes() {
return strDes;
}
/* 根据字符串获取订单状态枚举对象 */
public static OrderState getStateObj(String strOrderState) {
for (OrderState e : OrderState.values()) {
if (e.name().equals(strOrderState)) {
return e;
}
}
return null;
}
}
/**
*
* @Title: OrderConstant.java
* @Package com.boot.common.base
* @ClassName: OrderOperation
* @Description: 用户订单操作按钮
* @date 2019年3月12日 下午1:56:47
*/
public static enum OrderOperation {
delete("删除订单"),remindSend("提醒发货"),seeExpress("查看物流"),goPay("去支付"),applyRefund("申请退款"),cancelRefund("取消退款"),refuseCause("失败原因"),againBuy("再次购买"),
againExchange("再次兑换"),giveGift("赠送礼物"),againGive("再次送礼"),goBuy("我要送礼"),seeCoupon("查看票券"),delivery("去发货"),confirmOrder("确认订单"),seeDetail("查看详情"),
giveRecord("送礼记录"),selfReceive("自己领取"),giveGiftOthers("转赠他人"),applyDiscounting("申请折现"),endOrderOffline("确认收货");
private String strDes;// 订单状态描述
private OrderOperation(String strDes) {
this.strDes = strDes;
}
public String getStrDes() {
return strDes;
}
/* 根据字符串获取订单操作按钮枚举对象 */
public static OrderOperation getStateObj(String strOrderOperation) {
for (OrderOperation e : OrderOperation.values()) {
if (e.name().equals(strOrderOperation)) {
return e;
}
}
return null;
}
}
//枚举用途的测试
public static void main(String[] args) {
OrderState aaa=OrderConstant.OrderState.getStateObj("waitPay");
System.out.println(aaa.getStrDes());
}
}
打印结果: