携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情
枚举
枚举类型是java5引入的,由一组固定常量组成的合法类型。
在枚举引入之前如何定义一组常量
java在枚举引入之前,我们一般会用一组int常量值,来表示一组固定的数据。比如使用1、2、3、4来表示春、夏、秋、冬。
/**
* 枚举类型一般会被系统共享,所以其访问修饰符一般为public
*/
class Season {
public static final int SPRING = 1;
public static final int SUMMER = 2;
public static final int AUTUMN = 3;
public static final int WINTER = 4;
}
可以根据传入的int值来判断对应季节
@Test
public void test1() {
final int spring = Season.SPRING;
season(spring);
}
public void season(int value) {
switch (value) {
case 1:
System.out.println("春天");
break;
case 2:
System.out.println("夏天");
break;
case 3:
System.out.println("秋天");
break;
case 4:
System.out.println("冬天");
break;
default:
System.out.println("输入不合法");
break;
}
}
这种方法称作int枚举模式。存在一些安全问题,就如上面判断季节的方法,default分支是我们不愿意看到的场景,如果说我们不加校验可能会产生问题。并且Season这个类打印出来的也只是一个int值1、2、3、4,表面并不能看出任何的意思。所以说int枚举模式他的安全性和可读性是不可观的。
当然了我们也可以使用字符串作为枚举值,但是字符串的比较算法相对来说比较浪费性能,也是不可取的。
定义枚举
由于int枚举和字符串枚举存在着缺陷,java5引入了枚举类型
enum type
,接下来我们看如何定义一个枚举。
使用enum声明一个枚举,在枚举类中列举枚举值,使用逗号隔开,尾部使用分号结尾。
enum Season2 {
SPRING, SUMMER, AUTUMN, WINTER;
}
并且我们还可以为枚举定义属性:
@AllArgsConstructor
enum Season3 {
SPRING(1, "春天"),
SUMMER(1, "春天"),
AUTUMN(1, "春天"),
WINTER(1, "春天");
int code;
String msg;
}
特点
- 简约
- 和普通class类一样,枚举类可以单独存在,也可以存在于其他java类中
- 枚举类可以实现接口
- 也可以定义新的属性和方法
switch对于枚举的支持
使用枚举改造上面代码
public void seasonUseEnum(Season2 season) {
System.out.println(Season2.SPRING);
switch (season) {
case SPRING:
System.out.println("春天");
break;
case SUMMER:
System.out.println("夏天");
break;
case AUTUMN:
System.out.println("秋天");
break;
case WINTER:
System.out.println("冬天");
break;
default:
System.out.println("输入不合法");
break;
}
}
@Test
public void test2() {
seasonUseEnum(Season2.SPRING);
}
如此判断季节的方法对于传入参数存在类型限制,不会再有不合法参数的出现。一般来说我们会对枚举添加表示域的属性和对应的描述,方便统一管理。
public void seasonUseEnum(Season3 season) {
System.out.println(Season2.SPRING);
final StringBuilder sb = new StringBuilder();
switch (season) {
case SPRING:
case WINTER:
case AUTUMN:
case SUMMER:
sb.append(season.msg);
break;
default:
System.out.println("输入不合法");
break;
}
System.out.println(sb.toString());
}
@Test
public void test3() {
seasonUseEnum(Season3.SPRING);
}
jad查看原理
可以使用jad 反编译一下,查看一下底层原理
可以得出如下结论:
- 枚举类经过编译器编译后会被当作普通类处理,继承自
java.lang.Enum
- 每一个枚举项是一个
final static
的成员变量。天生是一个单例
final class Season3 extends Enum
{
private Season3(String s, int i, int code, String msg)
{
super(s, i);
this.code = code;
this.msg = msg;
}
public static final Season3 SPRING;
public static final Season3 SUMMER;
public static final Season3 AUTUMN;
public static final Season3 WINTER;
int code;
String msg;
private static final Season3 $VALUES[];
static
{
SPRING = new Season3("SPRING", 0, 1, "\u6625\u5929");
SUMMER = new Season3("SUMMER", 1, 1, "\u6625\u5929");
AUTUMN = new Season3("AUTUMN", 2, 1, "\u6625\u5929");
WINTER = new Season3("WINTER", 3, 1, "\u6625\u5929");
$VALUES = (new Season3[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}
}
但是要想知道switch对枚举的支持的原理,其实就在构造函数内,会调用super(s,i)。s是String类型为枚举项的字段名称,i为自动生成的编号。
我们使用jad对switch相关代码反编译一下:
- 首先枚举类中的每一个枚举都是一个单例对象,在使用new 关键字创建实例的时候会为各个实例添加一个编号 ordinal
- 在引用了枚举类的类中,会在static代码块中初始化一个int类型的数组,用于描述各个枚举值对应的编号
- switch还是对int做操作
{
static final int $SwitchMap$com$roily$booknode$javatogod$_01faceobj$javakeywords$aboutenum$Season3[];
static
{
$SwitchMap$com$roily$booknode$javatogod$_01faceobj$javakeywords$aboutenum$Season3 = new int[Season3.values().length];
$SwitchMap$com$roily$booknode$javatogod$_01faceobj$javakeywords$aboutenum$Season3[Season3.SPRING.ordinal()] int= 1;
$SwitchMap$com$roily$booknode$javatogod$_01faceobj$javakeywords$aboutenum$Season3[Season3.WINTER.ordinal()] = 2;
$SwitchMap$com$roily$booknode$javatogod$_01faceobj$javakeywords$aboutenum$Season3[Season3.AUTUMN.ordinal()] = 3;
$SwitchMap$com$roily$booknode$javatogod$_01faceobj$javakeywords$aboutenum$Season3[Season3.SUMMER.ordinal()] = 4;
}
}
public void seasonUseEnum(Season3 season)
{
System.out.println(Season2.SPRING);
StringBuilder sb = new StringBuilder();
switch(_cls1..SwitchMap.com.roily.booknode.javatogod._01faceobj.javakeywords.aboutenum.Season3[season.ordinal()])
{
case 1: // '\001'
case 2: // '\002'
case 3: // '\003'
case 4: // '\004'
sb.append(season.msg);
break;
default:
System.out.println("\u8F93\u5165\u4E0D\u5408\u6CD5");
break;
}
}
枚举是单例的最佳实践
单例的实现方式存在很多,懒汉式、饿汉式、双重检验锁、静态内部类、枚举
单例的设计主要考虑两个问题:
-
延时加载
在希望使用的时候才进行单例创建,在未正真使用不创建。那么双重检验锁、静态内部类符合需求
-
线程安全
单例实现的复杂问题在于需要考虑线程安全问题,同时兼虑性能。懒汉式非线程安全。
1、懒汉式可实现,但非线程安全
2、饿汉式不行,饿汉式单例的创建由类加载器实现,但线程安全
3、懒汉式配合Synchronized可实现,但影响性能(会对访问单例也进行加锁操作,但访问是没有线程安全问题的)
4、双重检验锁可延时加载:是对懒汉式+锁机制的优化。避免读时加锁
5、静态内部类可实现且线程安全,也是类加载器保证的线程安全
为何枚举是单例的最佳实现?
- 枚举天生单例,且线程安全,枚举作为内部类可实现延时加载。
- 枚举可避免序列化、或反射 破坏单例 (枚举的序列化是定制的,序列化时会将枚举项名记录,反序列化时会根据枚举项名称找到对应枚举项)
以上编写枚举反编译查看枚举中的每一个枚举项都被final static 修饰,且在static代码块中初始化,这也就是饿汉式单例的实现。
尝试使用反射破坏单例
我们知道单例的实现,重点在于构造函数私有化,并提供获取实例的方法,那么我们就来破坏构造方法的私有性
首先写一个单例类:
public class SingleDemo implements Serializable {
private static final long serialVersionUID = -6489201409969990006L;+
private static SingleDemo singleDemo;
//构造方法私有化
private SingleDemo() {
}
public static SingleDemo getInstance() {
if (null == singleDemo){
singleDemo = new SingleDemo();
}
return singleDemo;
}
}
@Test
public void testSingle() {
final SingleDemo instance1 = SingleDemo.getInstance();
final SingleDemo instance2 = SingleDemo.getInstance();
System.out.println(instance1 == instance2);
}
反射破坏单例:
final SingleDemo instance1 = SingleDemo.getInstance();
final Constructor<? extends SingleDemo> declaredConstructor = instance1.getClass().getDeclaredConstructor();
declaredConstructor.setAccessible(true);
final SingleDemo singleDemo = declaredConstructor.newInstance(null);
System.out.println(instance1 == singleDemo);
序列化破坏单例
先执行test1,再执行test2
@Test
public void test1() {
String filePath = "/Users/rolyfish/Desktop/MyFoot/myfoot/foot/testfile";
final SingleDemo instance1 = SingleDemo.getInstance();
try (final ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(new File(filePath, "object.txt")))) {
//将instance写入文件
objectOutputStream.writeObject(instance1);
objectOutputStream.flush();
} catch (IOException e) {
}
}
@Test
public void test2() {
String filePath = "/Users/rolyfish/Desktop/MyFoot/myfoot/foot/testfile";
final SingleDemo instance1 = SingleDemo.getInstance();
SingleDemo sngleDemo = null;
try (final ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File(filePath, "object.txt")))) {
//将instance写入文件
sngleDemo = (SingleDemo) objectInputStream.readObject();
} catch (IOException | ClassNotFoundException e) {
}
System.out.println(sngleDemo == instance1);
System.out.println(sngleDemo);
System.out.println(instance1);
}
枚举可避免以上问题
尝试使用反射破坏枚举单例:枚举的构造方法除枚举自定义的还有Enum类中的code。
会报出
IllegalArgumentException
异常,不可以使用反射创建枚举对象。
@Test
public void test2() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
final Constructor<? extends SignalEnum> declaredConstructor = SignalEnum.SIGNAL_ENUM.getClass().getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
final SignalEnum signalEnum = declaredConstructor.newInstance("signalEnum",2);
System.out.println(signalEnum == SignalEnum.SIGNAL_ENUM);
System.out.println(SignalEnum.SIGNAL_ENUM);
System.out.println(signalEnum);
}
尝试使用序列化破坏枚举单例
先执行testx 再执行testy
结果是true,表示序列化不会破坏枚举单例。
@Test
public void testx() {
String filePath = "/Users/rolyfish/Desktop/MyFoot/myfoot/foot/testfile";
final SignalEnum signalEnum = SignalEnum.SIGNAL_ENUM;
try (final ObjectOutputStream objectOutputStream = new ObjectOutputStream(
new FileOutputStream(new File(filePath, "object2.txt")))) {
//将instance写入文件
objectOutputStream.writeObject(signalEnum);
objectOutputStream.flush();
} catch (IOException e) {
}
}
@Test
public void testy() {
String filePath = "/Users/rolyfish/Desktop/MyFoot/myfoot/foot/testfile";
final SignalEnum signalEnum = SignalEnum.SIGNAL_ENUM;
SignalEnum signalEnum2 = null;
try (final ObjectInputStream objectInputStream = new ObjectInputStream(
new FileInputStream(new File(filePath, "object2.txt")))) {
//将instance写入文件
signalEnum2 = (SignalEnum) objectInputStream.readObject();
} catch (IOException | ClassNotFoundException e) {
}
System.out.println(signalEnum == signalEnum2);
System.out.println(signalEnum);
System.out.println(signalEnum2);
}
原因:
枚举类型拒绝反射创建实例。
枚举类型在序列话的时候会将枚举项,对应名称序列化,而具体信息不会序列话,在反序列化时,会根据枚举项名称,调用valueOf方法返回枚举项。