Java5.0新特性---枚举enum
前言:
写这篇随笔的时间已经是2020年8月份了,偷偷的去看了一下Oracle网站,发现java版本已经更新迭代到java14了,不禁感叹我对java5的相关知识还没有很好的掌握。java是一门非常活跃的语言,目前已经迭代到了java14版本,其中java5和java8被认为是java最具有里程碑的两个版本,java5中引入了泛型、自动拆装箱、增强for循环、可变参数、枚举等众多新特性,本篇随笔就简单写一下我理解的枚举。
一、枚举类的前世今生
java5之前,没有枚举enum关键字,我们如何实现枚举的功能?
public class Person { //定义两个成员变量,不对外提供set方法 private String name; private String description; /** * 有枚举意义的类,必须提供给外部有限个已经确定的对象,那么该类的构造器必须置为private * 让外部无法通过new的方式创建该对象,否则如果能通过new创建多个该对象,就不是枚举意义的类。 */private Person(String name,String description){ this.name = name; this.description = description; } @Override public String 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 void main(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 enum Person { /** * 提供两个枚举对象 * 1.final,不可被改变的,不允许外部对这两个对象进行更改操作 * 2.static,外部可以通过Person.MAN(类.静态变量)的方式得到该对象 */ MAN("男人","我是个男人"), //省去了 “private final static Person = new Person” WOMAN("女人","我是个女人"); //定义两个成员变量,不对外提供set方法 private String name; private String description; /** * 有枚举意义的类,必须提供给外部有限个已经确定的对象,那么该类的构造器必须置为private * 让外部无法通过new的方式创建该对象,否则如果能通过new创建多个该对象,就不是枚举意义的类。 */ Person(String name,String description){ //省去了private关键字 this.name = name; this.description = description; } /** * main测试 */ public static void main(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 void main(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 void main(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 enum Singleton { //该类唯一的一个实例 INSTANCE; /** * 供外部访问实例的方法 */ public static Singleton getInstance(){ return INSTANCE; } }
优点一:有效地避免了反射攻击
提到单例模式,可能我们都会想到饿汉模式(天生线程安全),懒汉模式(volatile+ 双重检测),这两种方式虽然都私有化了构造器,“希望”外部能根据公有方法获取该类的唯一实例。
但是,在有反射攻击的情况下,也只是希望了。先看一个暴力反射的例子
class Animal { //私有化构造方法 private Animal(){ } } class Test{ public static void main(String[] args) throws Exception { Class<Animal> clazz = Animal.class; //暴力反射,获取到Animal私有的无参构造方法 Constructor<Animal> constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); Animal animal = constructor.newInstance(); System.out.println(animal);//Animal@677327b6 } }
可以看到,即使Animal类私有化了构造方法,仍然能被反射获得其私有化的构造方法,完成对象的创建。
如果对枚举类使用反射攻击:
1 public enum Singleton { 2 3 //该类唯一的一个实例 4 INSTANCE; 5 6 /** 7 * 供外部访问实例的方法 8 */ 9 public static Singleton getInstance(){ 10 return INSTANCE; 11 } 12 13 } 14 15 class Test{ 16 17 public static void main(String[] args) throws Exception { 18 Class<Singleton> clazz = Singleton.class; 19 20 /** 21 * 暴力反射,获取到Singleton私有的无参构造方法 22 * 抛出异常:Exception in thread "main" java.lang.NoSuchMethodException 23 * 说明枚举类没有无参构造方法 24 */ 25 Constructor<Singleton> 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 enum Singleton { 2 3 //该类唯一的一个实例 4 INSTANCE; 5 6 /** 7 * 供外部访问实例的方法 8 */ 9 public static Singleton getInstance(){ 10 return INSTANCE; 11 } 12 13 } 14 15 class Test{ 16 17 public static void main(String[] args) throws Exception { 18 Class<Singleton> clazz = Singleton.class; 19 20 /** 21 * 暴力反射,获取到Singleton父类Enum的有参构造方法 22 */ 23 Constructor<Singleton> constructor = clazz.getDeclaredConstructor(String.class,int.class); 24 constructor.setAccessible(true); 25 26 /** 27 * 根据构造器创建对象 28 * java.lang.IllegalArgumentException: Cannot reflectively create enum objects 29 */ 30 Singleton singleton = constructor.newInstance("INSTANCE",1); 31 System.out.println(singleton); 32 33 } 34 }
这次虽然构造方法找到了,但是在30行代码创建实例的时候,抛出了不能反射创建对象的异常。为什么不能反射?原因在于:
1 public T newInstance(Object ... initargs) 2 throws InstantiationException, IllegalAccessException, 3 IllegalArgumentException, InvocationTargetException 4 { 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 return inst; 21 }
由此可见,使用枚举可以避免反射攻击。
优点二:是阻止反序列化时重新创建对象的一个有效方式(阻止序列化攻击)
先看一个序列化攻击案例:
1 public class Animal implements Serializable { 2 3 private static final long serialVersionUID = -7252563037661450268L; 4 5 private static Animal animal = new Animal(); 6 7 private Animal() { } 8 9 public static Animal getInstance() { 10 return animal; 11 } 12 } 13 14 class Test { 15 16 public static void main(String[] args) throws Exception { 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 enum Singleton { 2 3 //该类唯一的一个实例 4 INSTANCE; 5 6 /** 7 * 供外部访问实例的方法 8 */ 9 public static Singleton getInstance() { 10 return INSTANCE; 11 } 12 13 } 14 15 class Test { 16 17 public static void main(String[] args) throws Exception { 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方法也可以控制实例,但是枚举不香吗?