枚举
简介
枚举是
Java 5
中新增的一部分内容,他是一种特殊的类,一般表示一组常量,它和普通类一样可以使用构造器(必须私有
),定义成员变量和方法,也能实现一个或多个接口,但枚举类不能继承其他类
如何使用
1.语法
- 声明枚举类必须使用
enum
来实现,简洁语法如下:
enum-modifiers enum enumname {
enum-body,
}
复制代码
- 说明:
enum-modifiers
访问修饰符enumname
声明枚举类的名称enum-body
表示枚举的成员
2.举例
- 声明一个名叫Color的枚举类
// 1. 定义
public enum Color {
BLACK,
WHITE
}
// 2. 使用
class Test {
public static void main(String[] args) {
System.out.println(Color.BLACK); // BLACK
}
}
复制代码
3.底层原理
- 下面我们通过
jad
工具来反编译Color类, 通过jad -sjava Color.class
反编译出一份java文件
// final修饰,无法被继承
public final class Color extends Enum {
// 为了避免 返回的数组修改,而引起内部values值的改变,返回的是原数组的副本
public static Color[] values() {
return (Color[]) $VALUES.clone();
}
// 按名字获取枚举实例
public static Color valueOf(String name) {
return (Color) Enum.valueOf(em / Color, name);
}
// 私有的构造函数
private Color(String name, int ordinal) {
super(name, ordinal);
}
// enum第一行的声明的变量,都对应一个枚举实例对象
public static final Color BLACK;
public static final Color WHITE;
//
private static final Color $VALUES[];
// 静态域初始化,说明在类加载的cinit阶段就会被实例化,jvm能够保证类加载过程的线程安全
static {
BLACK = new Color("BLACK", 0);
WHITE = new Color("WHITE", 1);
$VALUES = (new Color[]{
BLACK, WHITE
});
}
}
复制代码
- 从反编译的
Color
类中可以看出,在enum关键字
的类中,第一行 (准确的说是第一个分号前) 定义的变量,都会生成一个Color实例
,且它是在静态域中进行初始化的, 而静态域在类加载阶段进行初始化,所以枚举对象是线程安全的,且由JVM来保证
ps: 这里我联想到八种单例模式的实现方式,其中一种就是使用enum
来实现的,那时候很是不能理解,它是怎么创建出对象的?仅仅是定义了一个常量吗? 大致代码如下
public enum Singleton {
INSTANCE;
public void doWhatever() {
// todo ...
}
}
复制代码
现在实际上我们可以知道,其实这个INSTANCE
其实反编译了之后是一个对象(public static final Singleton INSTANCE = new Singleton()
),且由于是JVM
控制的,所以有且只有一个对象实例,写法极其优雅
《Effective Java 》和《Java与模式》都非常推荐这种方式,使用这种方式方式实现枚举可以有什么好处呢?
《Effective Java》这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton的最佳方法。 —-《Effective Java 中文版 第二版》
《Java与模式》中,作者这样写道,使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。
4.枚举的唯一性
意思是枚举类型它拥有的实例在编写的时候,就已经确定下,不能通过其他手段进行创建,且枚举变量在jvm有且只有一个对应的实例 为了达到这种效果,这背后都做了些什么呢?
- 类加载时创建,保证线程安全
从Color类中可以看出,Color对象是在静态域创建,由类加载时初始化,JVM保证线程安全,这样就能确保Color对象不会因为并发同时请求而错误的创建多个实例.
- 对序列化进行特殊处理,防止反序列化时创建新的对象
我们知道一旦实现了Serializable接口之后,反序列化时每次调用readObject()
方法返回的都是一个新创建出来的对象
而枚举则不同,在序列化的时候Java仅仅是将枚举对象的name
属性输出到结果中,反序列化的时候则是通过Enum的valueOf()
方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化进行定制,因此禁用了writeObject
、readObject
、readObjectNoData
、writeReplace
和readResolve
等方法
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);
}
/**
* prevent default deserialization
*/
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
throw new InvalidObjectException("can't deserialize enum");
}
private void readObjectNoData() throws ObjectStreamException {
throw new InvalidObjectException("can't deserialize enum");
}
复制代码
- 有构造函数, 无法正常的 new出对象
就算我们没有使用 private
关键字进行修饰其默认访问修饰符依旧是 private
- 无法通过 clone()方法,克隆对象
/**
* Throws CloneNotSupportedException. This guarantees that enums
* are never cloned, which is necessary to preserve their "singleton"
* status.
*/
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
复制代码
- 无法通过反射的方式创建枚举对象
枚举类型,在 JVM 层面禁止了通过反射构造枚举实例的行为,如果尝试通过反射创建,将会报Cannot reflectively create enum objects
static void reflectTest() throws Exception {
// 获取类对象
Class<?> cls = Class.forName("com.xiao.basetest.base.enums.Color");
// 获取 color 的构造函数
Constructor<?> constructor = cls.getDeclaredConstructor(String.class, int.class);
// 获取访问权限
constructor.setAccessible(true);
// 实例化
Object reflectColor = constructor.newInstance("name", 0);
}
// 报错
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at Main.reflect(Main.java:24)
at Main.main(Main.java:13)
复制代码
常用方法
方法名称 | 描述 |
---|---|
values() | 以数组形式返回枚举类型的所有成员 |
valueOf() | 将普通字符串转换为枚举实例 |
compareTo() | 比较两个枚举成员在定义时的顺序 |
ordinal() | 获取枚举成员的索引位置 |
values()
循环输出成员变量,并使用ordinal()
输出索引值
public static void main(String[] args) {
for (Color value : Color.values()) {
System.out.println("当前索引值为:"+value.ordinal()+",数据为:"+value);
}
}
// 结果:
当前索引值为:0,数据为:BLACK
当前索引值为:1,数据为:WHITE
复制代码
- 通过
valueof()
获取枚举实例,并使用compareTo()
比较定义顺序
public enum Color {
BLACK,
WHITE,
YELLOW;
}
class Test {
public static void main(String[] args) {
Color color = Color.valueOf("WHITE");
compare(color);
}
public static void compare(Color color){
for (Color value : Color.values()) {
System.out.println(color+"与"+value+"的比较结果是"+color.compareTo(value));
}
}
}
结果:
WHITE与BLACK的比较结果是1
WHITE与WHITE的比较结果是0
WHITE与YELLOW的比较结果是-1
复制代码
枚举的成员变量与方法
- 除了枚举常量外, enum是一个完整的类,它也可以定义
成员变量
编写自己的构造方法
以及成员方法
,甚至实现接口
.
这里需要注意,枚举类不能继承其他类,因为在编译时它已经继承了Enum,java无法多继承
// 实现Runnable接口,在这个类中没有意义,只是为了举例
public enum Color implements Runnable {
WHITE("黑色",1),
BLACK("白色",2),
YELLOW("黄色",3);
private final String value; //定义成员变量
private final Integer index;
// 自定义构造,虽然没有写private,但是默认就是private
Color(String value,Integer index) {
this.value = value;
this.index = index;
}
// 自定义方法
public void draw() {
System.out.println("绘制 " + value);
}
// 重写方法
@Override
public String toString() {
return "hello I'm "+value+", my index is "+index;
}
// 实现接口方法
@Override
public void run() {
// todo ...
}
}
class Test {
public static void main(String[] args) {
for (Color value : Color.values()) {
System.out.println(value);
}
}
}
结果:
hello I'm 黑色, my index is 1
hello I'm 白色, my index is 2
hello I'm 黄色, my index is 3
复制代码
- 既然说到了方法,这里还要引申一下就是如果在枚举中使用了抽象方法,会发生什么呢?以及引出抽象方法后
枚举策略模式
相关说明
特定的常量类型与主体中的方法或行为有关时,即当数据与行为之间有关联时,可以考虑使用枚举来实现策略模式
假设我们在枚举类中定义一个抽象方法,那么它应该在哪个地方被实现呢,肯定是枚举实例变量中被实现,且必须实现
public enum Operation {
PLUS{
@Override
public int apply(int a, int b) {
return a+b;
}
},
MINUS{
@Override
public int apply(int a, int b) {
return a-b;
}
};
public abstract int apply(int a,int b);
public static void main(String[] args) {
System.out.println(Operation.PLUS.apply(1, 2));
}
}
复制代码
这种模式适用于当输入数据一样时,但需要做不同的操作时,可以采用这种方式来进行处理
复杂枚举
EnumSet,EnumMap并不常用,这里不做过多解释,想了解的可以参考 传送门
参考资料
csdn-Java枚举详解
菜鸟教程-枚举
简书-java枚举全面解读
本文由博客一文多发平台 OpenWrite 发布!