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方法也可以控制实例,但是枚举不香吗?

 

  

 

posted @ 2020-08-05 10:34  BoildWater  阅读(213)  评论(0编辑  收藏  举报