一、枚举简单使用
1、枚举的概念:
- 被enum关键字修饰的类型就是枚举类型
- 示例:
public enum Color {
YELLOW,
GREEEN,
RED;
}
- 枚举的优点: 将常量统一起来,进行统一管理。
- 场景: 错误码,状态机等
- 本质: 枚举的本质是 java.lang.enum 的子类。
2、枚举的常用方法:
- values() : 返回枚举的实例数组, 而且该数组的元素严格保持在 enum中声明的顺序。
- name() : 返回实例名字。
- ordinal(): 返回实例声明的顺序,从 0 开始。
- getDeclaringClass() : 返回实例所属的enum 类型
- equals(): 判断是否为同一个对象,内部实际上通过 “==”, 枚举是单例的, 通过地址判定即可
3、枚举特性:
- 枚举不能被继承,基本上可以看做一个常规的类。
- Java 不允许使用 = 为枚举常量赋值。
- enum 可以通过方法来显示赋值
- enum 可以添加普通方法、静态方法、抽象方法、构造方法。
- 枚举可以实现接口。
- 枚举不可以继承: 因为枚举实际上都是继承自 java.lang.Enum 类, 而Java不支持多重继承,所以 enum 不能继承其他类,自然也不能继承另外一个 enum
4、 枚举工具类:
- EnumSet: 是枚举类型的高性能 Set 实现,它要求放入它的枚举常量必须属于同一个枚举类型。
- EnumMap:是专门为枚举类型量身定做的 Map 实现。虽然使用其他的 Map 实现 (HashMap) 也能完成枚举类型实例到值得映射,但是使用EnumMap 会更高效; 它只能接收同一枚举类型的实例作为键值,并且由于枚举类型实例的数量相对固定并且有限, 所以使用 EnumMap 使用数组来存放与枚举类型对象的值。 这使得 EnumMap 更高效。
二、枚举问题解答:
1、枚举为什么不能继承、与被继承
我们以 Color 枚举为例:
public enum Color {
YELLOW,
GREEEN,
RED;
}
我们拿到 Color.java 编译后的文件 Color.class 使用使用命令:
javap Color.class
得到反编译的文件:
public final class com.lot.csdn.Color extends java.lang.Enum<com.lot.csdn.Color> {
public static final com.lot.csdn.Color YELLOW;
public static final com.lot.csdn.Color GREEEN;
public static final com.lot.csdn.Color RED;
public static com.lot.csdn.Color[] values();
public static com.lot.csdn.Color valueOf(java.lang.String);
static {};
}
从反编译的结果来看:
- 枚举被编译后生成的生成的普通的类
- 枚举 生成类的修饰符是 final 所以枚举不能被继承。
- 枚举生成类默认继承 java.lang.Enum 类, 由于 java 是单继承, 所以枚举类不能继承其他类。
- 由于继承 java.lang.Enum 所以枚举默认拥有其父类Enum中的方法。
2、枚举是如何实现单例的:
相关单例博客:https://blog.youkuaiyun.com/zhangyong01245/article/details/103323727
其中在Java 四大名著 《Effective Java》 中 第二章, 第3条 内容描述中:
- 从 Java 1.5 发行版本起,实现 Singletom 还有第三种方法。 只需编写一个包含单个元素的枚举类型:
// Enum singletom - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding(){........}
}
- 这种方法在功能上与公有域方法相近, 但是它更简洁,无偿地提供了序列化机制, 绝对防止多次实例化, 即使是在面对复杂的序列化或者反射攻击的时候都能实现有效的单例, 现在单元素的枚举类型已经成为实现 Singleton的最佳方法。
了解过单例模式的小伙伴,可以知道我们有时候可以通过 反射、反序列化 破坏系统中的单例, 那么, 枚举是如何应对上述问题的
3、 枚举是如何防止反射生成对象的:
我们以 Color.java 为例, 反射测试类如下:
public class TestColorReflection {
@SneakyThrows
public static void main(String[] args) {
Constructor<Color> constructor = Color.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
constructor.newInstance("YELLOW", 0);
}
}
执行结果, 抛出 IllegalArgumentException 异常, 提示: 不能反射创建枚举对象
我们进入到 Constructor.newInstance(Object … initargs) 中,箭头所指 if 判断已经阻止了我们对象 Enum 进行反射生成对象了。
4、枚举是如何防止反序列化生成新的对象的:
我先对 Color 枚举进行改造,添加上 name 属性, 方便后面进行总结:
public enum Color {
YELLOW("黄色"),
GREEEN("绿色"),
RED("红色");
public String name;
Color(String name) {
this.name = name;
}
}
测试序列化功能:
public class TestSerializable {
@SneakyThrows
public static void main(String[] args) {
// 将枚举写入到 文件中
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("testColor"));
out.writeObject(Color.YELLOW);
System.out.println("写入到文件中的 Color 对象 name:" + Color.YELLOW.name);
// 写入文件后, 我们改变 Color.YELLOW 名字
Color.YELLOW.name = "橙色";
// 从文件中读取枚举
ObjectInputStream in = new ObjectInputStream(new FileInputStream("testColor"));
Color color = (Color) in.readObject();
System.out.println("从文件中获取的 Color 对象 name:" + color.name);
}
}
执行后的结果,如下图:
呃呃呃… 我们写入文件时 Color 对象名字是 黄色, 为什么是读取出来是 橙色,
我们猜测一下: 反序列化时候没有拿文件中的对象而是直接取 Jvm 中的对象~
我们跟踪一下代码, 验证一下看看:
第一步断点: ObjectInputStream.readObject() 方法, 断点进入到箭头方法:
在 readObject0() 反序列化是枚举时会进入到 readEnum 方法:
在 readEnum 方法中已经很清楚了, 并不是从文件中读取 枚举的对象,而是直接从JVM中获取, 破案~