(五)从单例模式看懂枚举类(enum)

1.常见单例模式写法

/**
 * 饿汉模式
 */
public class Singleton1 {
    //私有静态不可变变量
    private static Singleton1 SINGLETON1 = new Singleton1();
    //私有构造函数
    private Singleton1() {
        System.out.println("Hello Singleton1");
    }
    //公共静态方法
    public static Singleton1 getInstance() {
        return SINGLETON1;
    }
}

/**
 * 懒汉模式
 */
public class Singleton2 {

    //私有静态不可变变量
    private static Singleton2 SINGLETON2;
    
    //私有构造函数
    private Singleton2() {
        System.out.println("Hello Singleton2.");
    }
    //公共静态方法
    public static Singleton2 getInstance() {
        if (SINGLETON2 == null) {
            SINGLETON2 = new Singleton2();
        }
        return SINGLETON2;
    }   
}
/**
 * 双重检验锁模式
 */
public class Singleton3 {
    // 私有静态不可变变量
    private volatile static Singleton3 SINGLETON3;
    // 私有构造函数
    private Singleton3() {
        System.out.println("Hello Singleton3.");
    }
    // 公共静态方法
    public static Singleton3 getInstance() {
        if (SINGLETON3 == null) {
            synchronized (Singleton3.class) {
                if (SINGLETON3 == null) {
                    SINGLETON3 = new Singleton3();
                }
            }
        }
        return SINGLETON3;
    }
}
/**
 * 内部静态类实现单例模式
 */
public class Singleton4 {   
    private static class SingletonHolder{
        private static final Singleton4 SINGLETON4 = new Singleton4();
    }
    private Singleton4() {
        System.out.println("Hello Singleton4.");
    }
    public static Singleton4 getInstance() {
        return SingletonHolder.SINGLETON4;
    }
}
/**
 * 枚举类实现
 */
public enum Singleton5 {
    INSTANCE;
    public void sayHello() {
        System.out.println("Hello Singleton5.");
    }
}

单例模式的特点:私有化的构造函数;私有的静态的全局变量;公有的静态的方法。

2.单例模式的问题

  • 通过反射机制可生成新的对象,破坏实例唯一性
    在《Effective Java》最佳实践第3条中提示:“享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。”
  • 单例类如果实现Serializable接口后可通过序列化与反序列化的方式生成新的对象,破坏实例唯一性
    在《Effective Java》最佳实践第77条中提示:“单例类如果实现了Serializable接口后它就不再是一个Singleton了。因为任何一个readObject方法,不管是显示的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。readResovle特性允许你用readObject创建的实例代替另一个实例。如果Singleton类要实现Serializable接口,则下面的readResovle方法可以满足它的Singleton特性。”
  // readResovle for instance control
  private Object readResovle() {
      return INSTANCE;
  }

该方法忽略了被序列化的对象,只返回该类初始化时创建的那个特殊的Singleton实例INSTANCE。如果依赖readResovle进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient的。否则,就有可能在readResovle方法被运行之前,保护指向反序列化对象的引用。

3.单例模式最佳实践

// 单例模式
public enum Singleton {
    INSTANCE;
}

4.从底层原理看懂枚举

public final class Singleton extends java.lang.Enum<Singleton> {
    public static final Singleton INSTANCE;
    public static Singleton [] values();
    public static Singleton valueOf(java.lang.String);
    static {};
}

上述代码为javap反编译得到的信息。从上述信息可知:

  • 编译器将枚举enum类型编译为final(不可变)类型的class类
  • 编译器对所有的枚举成员处理成public static final的枚举常量,并在静态域中进行初始化
  • 编译之后增加一个静态块,在此静态块中创建一个对象并将此对象赋值给静态对象
  • 编译器重新定义了构造器,为每个构造器都增加了两个参数和添加父类构造方法的调用;
  • 编译器为枚举类添加了 values() 和 valueOf()方法。values()方法返回一个枚举类型的数组,可用于遍历枚举类型。valueOf()方法返回一个枚举类型,可用于字符串转换成枚举类型。

4.枚举enum是实现单例模式的最佳实践

  • 编译器重新定义了构造器------防止反射攻击
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public enum Singleton {
    
    INSTANCE;

    public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException,
            IllegalAccessException, IllegalArgumentException, InvocationTargetException {

        Singleton singleton1 = Singleton.INSTANCE;
        Singleton singleton2 = Singleton.INSTANCE;
        System.out.println("正常情况下,实例化两个实例,判断它们是否相等:" + (singleton1 == singleton2));
        
        // constructor中没有无参构造器只有一个参数为(String.class,int.class)构造器
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton singleton3 = constructor.newInstance();

        System.out.println("通过反射攻击单例模式情况下,实例化两个实例,判断它们是否相等:" + (singleton1 == singleton3));

    }
}
输出结果如下:
正常情况下,实例化两个实例,判断它们是否相等:true
Exception in thread "main" java.lang.NoSuchMethodException: com.nwpu.davince.enumeration.Singleton.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at com.nwpu.davince.enumeration.Singleton.main(Singleton.java:17)

让我们看看到底哪里出了问题?通过debug模式查看 Singleton.class.getDeclaredConstructor()方法得知,返回结果没有无参构造函数只有一个(String name, int ordinal)类型的构造函数,我们看一下Enum的源码就会明白,编译器重新定义的构造器继承自Enum枚举抽象类。

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
   
    private final String name;

    public final String name() {
        return name;
    }

    private final int ordinal;

    public final int ordinal() {
        return ordinal;
    }

    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
    // 省略其余代码
}

讲道理,刚才抛出的异常是因为Singleton枚举拿不到无参构造函数,但Singleton可以拿到父类Enum的构造函数,我们可以来试试看,是否是真的因为没有构造函数导致异常抛出来的。

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public enum Singleton {
    
    INSTANCE;

    public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException,
            IllegalAccessException, IllegalArgumentException, InvocationTargetException {

        Singleton singleton1 = Singleton.INSTANCE;
        Singleton singleton2 = Singleton.INSTANCE;
        System.out.println("正常情况下,实例化两个实例,判断它们是否相等:" + (singleton1 == singleton2));
        
        // constructor中没有我们无参构造器只有一个参数为(String.class,int.class)构造器
        // Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        // 获取父类Enum的构造函数
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(String.class,int.class);
        constructor.setAccessible(true);
        Singleton singleton3 = constructor.newInstance();

        System.out.println("通过反射攻击单例模式情况下,实例化两个实例,判断它们是否相等:" + (singleton1 == singleton3));

    }
}
输出结果如下:
正常情况下,实例化两个实例,判断它们是否相等:true
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at com.nwpu.davince.enumeration.Singleton.main(Singleton.java:22)

我们来看看at java.lang.reflect.Constructor.newInstance(Constructor.java:417)这行错误抛出的源代码:

    @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        // 反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }
  • 避免序列化问题
    Talk is cheap,show me code!
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public enum Singleton implements Serializable{
    
    INSTANCE;
    
    private String message;
    
    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
    
    private Singleton() {
        
    }
    
    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        Singleton singleton = Singleton.INSTANCE;
        singleton.setMessage("单例枚举序列化");
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.obj"));
        oos.writeObject(singleton);
        oos.flush();
        oos.close();
        
        FileInputStream fis = new FileInputStream("Singleton.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Singleton singleton2 = (Singleton) ois.readObject();
        ois.close();
        
        System.out.println("序列化出来后的信息为:" + singleton2.getMessage());
        System.out.println("序列化前后的两个对象是否相等:" + (singleton == singleton2));
    }
}

输出结果为:
序列化出来后的信息为:单例枚举序列化
序列化前后的两个对象是否相等:true

原创不易,如需转载,请注明出处@author Davince!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容