Java中的枚举
有时候,变量的取值只在一个有限的集合内。例如:销售的服装或比萨饼只有小、中、大和超大这四种尺寸。当然,可以将这些尺寸分别编码为 1、2、3、4 或 S、M、L、X。但这样存在着一定的隐患。在变量中很可能保存的是一个错误的值(如 0 或 m)。
针对这种情况,可以自定义枚举类型。枚举类型包括有限个命名的值。
——引用自《Java 核心技术卷 1 第十版》
1. 枚举的定义
在 JDK 1.5 发布时正式发布的枚举类型,在某些情景中大大的简化了我们的开发。
举一个简单的例子:星期来定义枚举类。
1.1 传统的常量定义方式
如果不使用枚举,我们可能会这样子来定义:
public class WeekConstant {
public static final Integer WEEK_MONDAY = 1;
public static final Integer WEEK_TUESDAY = 2;
public static final Integer WEEK_WEDNESDAY = 3;
public static final Integer WEEK_THURSDAY = 4;
public static final Integer WEEK_FRIDAY = 5;
public static final Integer WEEK_SATURDAY = 6;
public static final Integer WEEK_SUNDAY = 7;
}
我们在使用时
@Slf4j
public class WeekTest {
public static void main(String[] args) {
log.info(new WeekTest().showDesc(1));
log.info(new WeekTest().showDesc(0));
}
public String showDesc(int num) {
switch (num) {
case WeekConstant.WEEK_MONDAY:
return "我是周一";
case WeekConstant.WEEK_TUESDAY:
return "我是周二";
case WeekConstant.WEEK_WEDNESDAY:
return "我是周三";
case WeekConstant.WEEK_THURSDAY:
return "我是周四";
case WeekConstant.WEEK_FRIDAY:
return "我是周五";
case WeekConstant.WEEK_SATURDAY:
return "我是周六";
case WeekConstant.WEEK_SUNDAY:
return "我是周日";
default:
return "未知";
}
}
}
运行起来也没问题。 但是, 我们就如同上面第二种调用方式一样, 其实我们的方向就在 7 种范围之内,但在调用的时候传入不是 1~7 之间的一个 int 类型的数据, 编译器是不会检查出来的。
1.2 枚举方法
我们现在使用枚举来实现上面的功能
定义:
public enum WeekEnum {
/**
* 星期
*/
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
测试:
@Slf4j
public class WeekTest2 {
public static void main(String[] args) {
log.info(new WeekTest2().showDesc(WeekEnum.SATURDAY));
// new WeekTest2().showDesc(1); // 编译错误
}
public String showDesc(WeekEnum num) {
switch (num) {
case MONDAY:
return "我是周一";
case TUESDAY:
return "我是周二";
case WEDNESDAY:
return "我是周三";
case THURSDAY:
return "我是周四";
case FRIDAY:
return "我是周五";
case SATURDAY:
return "我是周六";
case SUNDAY:
return "我是周日";
default:
return "未知";
}
}
}
以上只是一个举的例子, 其实, 枚举中可以很方便的获取自己的名称。
通过使用枚举, 我们可以很方便的限制了传入的参数, 如果传入的参数不是我们指定的类型, 则就发生错误。
1.3 定义总结
1.3.1相比于枚举,使用常量类的几个缺陷:
- 类型不安全。若一个方法中要求传入季节这个参数,用常量的话,形参就是int类型,开发者传入任意类型的int类型值就行,但是如果是枚举类型的话,就只能传入枚举类中包含的对象。
- 没有命名空间。假如要表达季节性的词语,开发者可能要在命名的时候以
SEASON_
开头,这样另外一个开发者再看这段代码的时候,才知道这四个常量分别代表季节。
1.3.2 简单小结
- 枚举类型的定义跟类一样, 只是需要将 class 替换为 enum
- 枚举名称与类的名称遵循一样的惯例来定义
- 枚举值由于是常量, 一般推荐全部是大写字母
- 多个枚举值之间使用逗号分隔开
- 最好是在编译或设计时就知道值的所有类型, 比如上面的方向, 当然后面也可以增加
2. 枚举的本质
将上面的代码反编译,可以发现以下几个特点
2.1 继承 java.lang.Enum
通过以上的反编译, 我们知道了, java.lang.Enum
是所有枚举类型的基类。查看其定义
public abstract class Enum>
implements Comparable, Serializable {
可以看出来, java.lang.Enum 有如下几个特征
- 抽象类, 无法实例化
- 实现了 Comparable 接口, 可以进行比较
- 实现了 Serializable 接口, 可进行序列化
因此, 相对应的, 枚举类型也可以进行比较和序列化
2.2 final 类型
final 修饰, 说明枚举类型是无法进行继承的
2.3 枚举常量本身就是该类的实例对象
可以看到, 我们定义的常量, 在类内部是以实例对象存在的, 并使用静态代码块进行了实例化。
2.4 构造函数私有化
不能像正常的类一样, 从外部 new 一个对象出来。
2.5 添加了 $values[] 变量及两个方法
- $values[]: 一个类型为枚举类本身的数组, 存储了所有的示例类型
- values() : 获取以上所有实例变量的克隆值
- valueOf(): 通过该方法可以通过名称获得对应的枚举常量
3. 枚举的一般使用
枚举默认是有几个方法的。
3.1 类本身的方法
类本身有两个方法,是编译时添加的
3.1.1 values()
返回的是枚举常量的克隆数组。
使用示例
final WeekEnum[] values = WeekEnum.values();
for (WeekEnum value : values) {
log.info(value.name());
}
输出
15:23:05.948 [main] INFO com.kjgym.javadavanced.meiju.WeekTest2 - MONDAY
15:23:05.951 [main] INFO com.kjgym.javadavanced.meiju.WeekTest2 - TUESDAY
15:23:05.951 [main] INFO com.kjgym.javadavanced.meiju.WeekTest2 - WEDNESDAY
15:23:05.951 [main] INFO com.kjgym.javadavanced.meiju.WeekTest2 - THURSDAY
15:23:05.951 [main] INFO com.kjgym.javadavanced.meiju.WeekTest2 - FRIDAY
15:23:05.951 [main] INFO com.kjgym.javadavanced.meiju.WeekTest2 - SATURDAY
15:23:05.951 [main] INFO com.kjgym.javadavanced.meiju.WeekTest2 - SUNDAY
3.1.2 valueOf(String)
该方法通过字符串获取对应的枚举常量.
代码示例
final WeekEnum weekEnum = WeekEnum.valueOf("MONDAY");
log.info(weekEnum.name() + "\t" + weekEnum.ordinal());
输出
15:26:02.460 [main] INFO com.kjgym.javadavanced.meiju.WeekTest2 - MONDAY 0
3.2 继承的方法
因为枚举类型继承于 java.lang.Enum
, 因此除了该类的私有方法, 其他方法都是可以使用的。
3.2.1 ordinal()
该方法返回的是枚举实例的在定义时的顺序, 类似于数组, 第一个实例该方法的返回值为 0。
在基于枚举的复杂数据结构 EnumSet和EnumMap 中会用到该函数。
log.info(WeekEnum.MONDAY.ordinal()); // 输出 0
log.info(WeekEnum.FRIDAY.ordinal()); // 输出 4
3.2.2 compareTo()
该方法时实现的 Comparable 接口的, 其实现如下。
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;
}
首先, 需要枚举类型是同一种类型, 然后比较他们的 ordinal 来得出大于、小于还是等于。
System.out.println(WeekEnum.FRIDAY.compareTo(WeekEnum.SATURDAY)); // -1
System.out.println(WeekEnum.FRIDAY.compareTo(WeekEnum.MONDAY)); // 4
3.2.3 name() 和 toString()
该两个方法都是返回枚举常量的名称。 但是, name()
方法为 final 类型, 是不能被覆盖的! 而 toString 可以被覆盖。
3.2.4 getDeclaringClass()
获取对应枚举类型的 Class 对象。
System.out.println(WeekEnum.SATURDAY.getDeclaringClass());
测试结果
class com.kjgym.javadavanced.meiju.WeekEnum
3.2.5 eques()
判断指定对象与枚举常量是否相同。
4. 枚举类型进阶
4.1 自定义构造函数
首先, 定义的构造函数可以是 private, 或不加修饰符
我们重新定义星期枚举类为:
public enum WeekEnum {
/**
* 星期
*/
MONDAY(1, "我是周一"),
TUESDAY(2, "我是周二"),
WEDNESDAY(3, "我是周三"),
THURSDAY(4, "我是周四"),
FRIDAY(5, "我是周五"),
SATURDAY(6, "我是周六"),
SUNDAY(7, "我是周日");
/**
* 码
*/
private int code;
/**
* 描述
*/
private String desc;
public int getCode() {
return code;
}
public String getDesc() {
return desc;
}
Test03(int code, String desc) {
this.code = code;
this.desc = desc;
}
}
测试
System.out.println(Test03.FRIDAY.getDesc()); // 我是周五
4.2 添加自定义的方法
4.2.1 自定义具体方法
我们在枚举类型内部加入如下具体方法
protected void show() {
System.out.println("It is " + this.getDesc());
}
测试
Test03.SUNDAY.show();
结果
It is 我是周日
4.2.2 在枚举中定义抽象方法
@Getter
@AllArgsConstructor
@ToString
public enum Test03 {
/**
* 星期
*/
MONDAY(1, "我是周一") {
@Override
String getName() {
return this.name() + this.ordinal();
}
},
TUESDAY(2, "我是周二") {
@Override
String getName() {
return this.name() + this.ordinal();
}
},
WEDNESDAY(3, "我是周三") {
@Override
String getName() {
return this.name() + this.ordinal();
}
},
THURSDAY(4, "我是周四") {
@Override
String getName() {
return this.name() + this.ordinal();
}
},
FRIDAY(5, "我是周五") {
@Override
String getName() {
return this.name() + this.ordinal();
}
},
SATURDAY(6, "我是周六") {
@Override
String getName() {
return this.name() + this.ordinal();
}
},
SUNDAY(7, "我是周日") {
@Override
String getName() {
return this.name() + this.ordinal();
}
};
/**
* 码
*/
private int code;
/**
* 描述
*/
private String desc;
abstract String getName();
}
简单测试
System.out.println(Test03.SUNDAY.getName());
测试结果:SUNDAY6
4.3 覆盖父类方法
在父类 java.lang.Enum 中, 也就只有 toString() 是没有使用 final 修饰啦, 要覆盖也只能覆盖该方法。
4.4 实现接口
因为Java是单继承的, 因此, Java中的枚举因为已经继承了 java.lang.Enum, 因此不能再继承其他的类。
但Java是可以实现多个接口的, 因此 Java 中的枚举也可以实现接口。
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);
}
}
5. 使用枚举实现单例
该方法是在 《Effective Java》 提出的
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}
单例模式的实现有很多种,网上也分析了如今实现单利模式最好用枚举,好处不外乎三点:
1.线程安全
2.不会因为序列化而产生新实例
3.防止反射攻击
但是貌似没有一篇文章解释ENUM单例如何实现了上述三点,请高手解释一下这三点:
关于第一点线程安全,从反编译后的类源码中可以看出也是通过类加载机制保证的,应该是这样吧(解决)
关于第二点序列化问题,有一篇文章说枚举类自己实现了readResolve()方法,所以抗序列化,这个方法是当前类自己实现的(解决)
关于第三点反射攻击,我有自己试着反射攻击了以下,不过报错了...看了下方的反编译类源码,明白了,因为单例类的修饰是abstract的,所以没法实例化。(解决)
该方法无论是创建还是调用, 都是很简单。
单元素的枚举类型已经成为实现Singleton的最佳方法。
——取自《Effective Java》