前言:
写这篇随笔的时间已经是2020年8月份了,偷偷的去看了一下Oracle网站,发现java版本已经更新迭代到java14了,不禁感叹我对java5的相关知识还没有很好的掌握。java是一门非常活跃的语言,目前已经迭代到了java14版本,其中java5和java8被认为是java最具有里程碑的两个版本,java5中引入了泛型、自动拆装箱、增强for循环、可变参数、枚举等众多新特性,本篇随笔就简单写一下我理解的枚举。
一、枚举类的前世今生
java5之前,没有枚举enum关键字,我们如何实现枚举的功能?
public classPerson {//定义两个成员变量,不对外提供set方法
privateString name;privateString description;/*** 有枚举意义的类,必须提供给外部有限个已经确定的对象,那么该类的构造器必须置为private
* 让外部无法通过new的方式创建该对象,否则如果能通过new创建多个该对象,就不是枚举意义的类。*/privatePerson(String name,String description){this.name =name;this.description =description;
}
@OverridepublicString toString() {return "Person{" +
"name='" + name + '\'' +
", description='" + description + '\'' +
'}';
}/*** 自身提供两个枚举对象
* 1.final,不可被改变的,不允许外部对这两个对象进行更改操作
* 2.static,外部可以通过Person.MAN(类.静态变量)的方式得到该对象*/
public final static Person MAN = new Person("男人","我是个男人");public final static Person WOMAN = new Person("女人","我是个女人");/*** main测试*/
public static voidmain(String[] args) {
Person person1=Person.MAN;
Person person2=Person.WOMAN;
System.out.println(person1);//Person{name='男人', description='我是个男人'}
System.out.println(person2);//Person{name='女人', description='我是个女人'}
}
}
java5有了关键字enum如何定义枚举类?很多技术的发展与迭代跟人懒离不开关系,上面这个具有枚举意义的类,在有了enum关键字后,一些必要且重复的成分就可以省去了。
public enumPerson {/*** 提供两个枚举对象
* 1.final,不可被改变的,不允许外部对这两个对象进行更改操作
* 2.static,外部可以通过Person.MAN(类.静态变量)的方式得到该对象*/MAN("男人","我是个男人"), //省去了 “private final static Person = new Person”
WOMAN("女人","我是个女人");//定义两个成员变量,不对外提供set方法
privateString name;privateString description;/*** 有枚举意义的类,必须提供给外部有限个已经确定的对象,那么该类的构造器必须置为private
* 让外部无法通过new的方式创建该对象,否则如果能通过new创建多个该对象,就不是枚举意义的类。*/Person(String name,String description){//省去了private关键字
this.name =name;this.description =description;
}/*** main测试*/
public static voidmain(String[] args) {
Person person1=Person.MAN;
Person person2=Person.WOMAN;
System.out.println(person1.toString());//MAN
System.out.println(person2.toString());//WOMAN
}
}
有了enum,创建一个枚举类是不是更简单了呢?
二、Enum类
上面使用enum关键字创建枚举类的代码中,我特意删除了toString()方法,而在main测试的结果中,person1、person2分别打印出了“MAN”、“WOMAN”字符串。
我们都知道如果一个类被创建的时候,即使没有显示地指定继承父类Object类时,实际上也会将Object作为父类,拥有Object类中的toString()、equals()、hashcode()等方法。
就拿toString()方法来说,如果子类中没有去重写该方法,那么子类对象调用toString()方法时,打印出的应该会是一个地址值,类似于Person@1b6d3586,然而却打印出了“MAN”、“WOMAN”。
这说明了什么?要么被enum修饰的类,默认进行了隐式的toString()方法重写,又或者被enum修饰的类的父类,另有其“类”,不是直接继承于Object,在该父类中对toString()方法进行了重写。
到底真相是什么?
/*** main测试*/
public static voidmain(String[] args) {
Person person1=Person.MAN;
Person person2=Person.WOMAN;
System.out.println(person1.toString());//MAN
System.out.println(person2.toString());//WOMAN
/*** 通过反射,验证了枚举类Person的直接父类不是Object而是java.lang.Enum*/Class> superclass =person1.getClass().getSuperclass();
System.out.println(superclass);//class java.lang.Enum
}
哦,原来枚举类都会默认继承java.lang.Enum这个类,那来看看这个类提供了哪些子类可以调用的方法?
/*** main测试*/
public static voidmain(String[] args) {//获取Person枚举类定义的所有对象
Person[] values =Person.values();for(Person person : values){
System.out.println(person);
}//根据枚举对象名获取枚举对象,注意:如果获取不到,将会抛出异常,而不是返回null值。
Person person1 = Person.valueOf("MAN");
System.out.println(person1);//使用父类Enum提供的方法,获取指定类型的枚举对象
Person person2 = Enum.valueOf(Person.class, "WOMAN");
System.out.println(person2);
//......
}
看了上面代码,细心的同学,可能又会发现了,java.lang.Enum类中并没有 values()、valueOf(String name)的两个静态方法,而Person这个枚举类中,也没有定义这两个方法。
那么这两个方法从何而来?反编译一下看看。
可以看到,在创建该类的时候,编译器自动加上了静态的values()和valueOf()方法。甚至还有意外的发现,反编译后可以看到枚举类是final的,所以枚举类也具有final修饰的类的相关特性。
三、枚举类的应用
1.Java中很多类都使用到了枚举,比如Thread类中用于定义线程状态的public enum State枚举类等。
2.常见的其他应用,单例模式---枚举实现,这个重点说一下。
public enumSingleton {//该类唯一的一个实例
INSTANCE;/*** 供外部访问实例的方法*/
public staticSingleton getInstance(){returnINSTANCE;
}
}
优点一:有效地避免了反射攻击
提到单例模式,可能我们都会想到饿汉模式(天生线程安全),懒汉模式(volatile+ 双重检测),这两种方式虽然都私有化了构造器,“希望”外部能根据公有方法获取该类的唯一实例。
但是,在有反射攻击的情况下,也只是希望了。先看一个暴力反射的例子
classAnimal {//私有化构造方法
privateAnimal(){
}
}classTest{public static void main(String[] args) throwsException {
Class clazz = Animal.class;//暴力反射,获取到Animal私有的无参构造方法
Constructor constructor =clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Animal animal=constructor.newInstance();
System.out.println(animal);//Animal@677327b6
}
}
可以看到,即使Animal类私有化了构造方法,仍然能被反射获得其私有化的构造方法,完成对象的创建。
如果对枚举类使用反射攻击:
1 public enumSingleton {2
3 //该类唯一的一个实例
4 INSTANCE;5
6 /**
7 * 供外部访问实例的方法8 */
9 public staticSingleton getInstance(){10 returnINSTANCE;11 }12
13 }14
15 classTest{16
17 public static void main(String[] args) throwsException {18 Class clazz = Singleton.class;19
20 /**
21 * 暴力反射,获取到Singleton私有的无参构造方法22 * 抛出异常:Exception in thread "main" java.lang.NoSuchMethodException23 * 说明枚举类没有无参构造方法24 */
25 Constructor constructor =clazz.getDeclaredConstructor();26 constructor.setAccessible(true);27
28 //根据构造器创建对象
29 Singleton singleton =constructor.newInstance();30 System.out.println(singleton);31
32 }33 }
在上面25行位置抛出了java.lang.NoSuchMethodException,说明了枚举类没有提供无参的构造方法,这也是一种保护。
但是,枚举类父类Enum类中还存在一个protected Enum(String name, int ordinal)有参构造方法,使用该方法,看看能否通过反射获取对象。
1 public enumSingleton {2
3 //该类唯一的一个实例
4 INSTANCE;5
6 /**
7 * 供外部访问实例的方法8 */
9 public staticSingleton getInstance(){10 returnINSTANCE;11 }12
13 }14
15 classTest{16
17 public static void main(String[] args) throwsException {18 Class clazz = Singleton.class;19
20 /**
21 * 暴力反射,获取到Singleton父类Enum的有参构造方法22 */
23 Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);24 constructor.setAccessible(true);25
26 /**
27 * 根据构造器创建对象28 * java.lang.IllegalArgumentException: Cannot reflectively create enum objects29 */
30 Singleton singleton = constructor.newInstance("INSTANCE",1);31 System.out.println(singleton);32
33 }34 }
这次虽然构造方法找到了,但是在30行代码创建实例的时候,抛出了不能反射创建对象的异常。为什么不能反射?原因在于:
1 publicT newInstance(Object ... initargs)2 throwsInstantiationException, IllegalAccessException,3 IllegalArgumentException, InvocationTargetException4 {5 if (!override) {6 if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {7 Class> caller =Reflection.getCallerClass();8 checkAccess(caller, clazz, null, modifiers);9 }10 }11 //此处说明了原因,如果构造函数是一个枚举类型,抛出异常。
12 if ((clazz.getModifiers() & Modifier.ENUM) != 0)13 throw new IllegalArgumentException("Cannot reflectively create enum objects");14 ConstructorAccessor ca = constructorAccessor; //read volatile
15 if (ca == null) {16 ca =acquireConstructorAccessor();17 }18 @SuppressWarnings("unchecked")19 T inst =(T) ca.newInstance(initargs);20 returninst;21 }
由此可见,使用枚举可以避免反射攻击。
优点二:是阻止反序列化时重新创建对象的一个有效方式(阻止序列化攻击)
先看一个序列化攻击案例:
1 public class Animal implementsSerializable {2
3 private static final long serialVersionUID = -7252563037661450268L;4
5 private static Animal animal = newAnimal();6
7 privateAnimal() { }8
9 public staticAnimal getInstance() {10 returnanimal;11 }12 }13
14 classTest {15
16 public static void main(String[] args) throwsException {17 Animal instance =Animal.getInstance();18
19 //序列化对象
20 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serializeFile"));21 oos.writeObject(instance);22
23 //再从序列化文件中反序列化出对象
24 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("serializeFile"));25 Animal instance1 =(Animal) ois.readObject();26
27 //比较两个对象是否相同
28 System.out.println(instance == instance1);//false
29 }30 }
在上面第28行代码中,可以看到,通过序列化和反序列化后,得到了两个不同的对象,就违背了单例的初衷。
原因在于:“任何一个 readObject 方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例”,关于这句话的详细介绍,请百度查看。
那么如果反序列化枚举类对象,会发生什么呢?
1 public enumSingleton {2
3 //该类唯一的一个实例
4 INSTANCE;5
6 /**
7 * 供外部访问实例的方法8 */
9 public staticSingleton getInstance() {10 returnINSTANCE;11 }12
13 }14
15 classTest {16
17 public static void main(String[] args) throwsException {18 Singleton instance =Singleton.getInstance();19
20 //序列化对象
21 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serializeFile"));22 oos.writeObject(instance);23
24 //再从序列化文件中取出该对象
25 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("serializeFile"));26 Singleton instance1 =(Singleton) ois.readObject();27
28 //比较两个对象是否相同
29 System.out.println(instance == instance1);//true
30 }31 }
可以看到在29行,打印的结果显示反序列化枚举类对象后,得到的对象与序列化前的对象是相同的。
正如“对于实例控制,枚举类型优先于readResolve”所说一样,虽然重写readResolve方法也可以控制实例,但是枚举不香吗?