《Effective Java》学习笔记3 Enforce the singleton property with a private constructor or an enum

版权声明:欢迎转载,但麻烦注明出处 https://blog.csdn.net/q2878948/article/details/81324713

本栏是博主根据如题教材进行Java进阶时所记的笔记,包括对原著的概括、理解,教材代码的报错和运行情况。十分建议看过原著遇到费解地方再来参考或与博主讨论。致敬作者Joshua Bloch跟以杨春花为首的译者团队,以及为我提供可参考博文的博主们。

用私有构造器或者枚举类型强化Singleton属性 

单例Singleton一般被用来表示程序中本质上唯一的实例。单例的测试有些困难,因为不能用模拟实现代替单例,除非它实现了充当其类型的接口。

实现单例有两种常见的方法。两者都是基于私有化构造函数和导出公共静态成员以提供对唯一实例的访问。

第一种方法

该单例的实例是final型的,并且可以从外部直接访问它:

package tradiationalsingleton;

/**
 * 这种方式看似安全,实则暗藏杀机。拥有特权的客户端可以通过AccessibleObject.setAccessible()
 * 通过反射机制来调用其私有构造器、访问私有变量私有或者使用私有方法,
 * 除非修改构造方法的逻辑,让它在试图创建第二个实例的时候抛出异常{@link ModifyPrivateDataTest}
 *
 * 公有域的优势在于可读性好,final的存在确保所有的对象引用都是相同的,
 * 但其在性能上不具有任何优势,因为现代JVM机制几乎都可以将静态工厂方法的调用内联化
 *      注:调用的内联化(inline),不再保存现场跳转函数执行结束后返回,
 *      而是直接将函数执行过程放在调用它的地方,尤其适用于小型的方法.
 *
 */
public class UnsafeSingleton {

    public static final UnsafeSingleton OUR_INSTANCE = new UnsafeSingleton();

    public void leaveTheBuilding(){
        System.out.println("Whoa baby, I'm outta here!");
    }
    private UnsafeSingleton() {
        super();
    }

    public static void main(String[] args) {
        UnsafeSingleton.OUR_INSTANCE.leaveTheBuilding();
    }
}

第二种方式

是将静态final型实例设置为private型,然后对外提供静态工厂方法:

package tradiationalsingleton;

/**
 * 这个也是IDEA默认创建singleton时自动生成的代码,但仍然存在{@link UnsafeSingleton}中的隐患
 *
 * 静态工厂方法的优势在于其灵活性:在API不变的前提下,我们可以改变该类是否具有Singleton属性。
 * 比如,我们可以根据变化的需求将其更改为每个调用该方法的线程拥有唯一的实例
 * //其实好像还有其他优势,参见《Effective Java》,但内容在比较靠后的位置,暂时先不管
 *
 * 对于序列化单例的说明见{@link serizablesingleton.UnexpectedSingleton}
 * 和{@link serizablesingleton.SerializableSingleton}
 */
public class UnsafeSingleton2 {

    private static final UnsafeSingleton2 ourInstance = new UnsafeSingleton2();

    public static UnsafeSingleton2 getInstance() {
        return ourInstance;
    }

    public void leaveTheBuilding(){
        System.out.println("Whoa baby, I'm outta here!");
    }

    private UnsafeSingleton2() {
        super();
    }
}

序列化问题

并且,上面两种单例往往会在序列化时出现问题。

将单例变成可序列化的,仅仅加上implements Serializable还不够,需要声明所有实例域都是瞬时的,并且提供readResolve方法,否则每次对序列化实例进行反序列化时,都会创建一个新的实例

package serizablesingleton;

import java.io.*;

/**
 * 这样的代码经过序列化、反序列化之后,单例的唯一性再次出现问题:本应是同一对象的判别出现了问题(见最后一句)
 *
 * 问题出在哪里?————反序列化的ObjectInputStream类。
 * 这个类的{@link ObjectInputStream#readOrdinaryObject(boolean)}方法中会判断是否可以实例化,
 * 如果可以的话,会通过反射机制调用无参的构造方法创建新实例,而不去管是不是private的
 */
public class UnexpectedSingleton implements Serializable{

    private static UnexpectedSingleton ourInstance = new UnexpectedSingleton();

    public static UnexpectedSingleton getInstance() {
        return ourInstance;
    }

    private UnexpectedSingleton() {
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //write
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempfile"));
        oos.writeObject(UnexpectedSingleton.getInstance());
        //read
        File file = new File("tempfile");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        UnexpectedSingleton deserializedSingleton = (UnexpectedSingleton) ois.readObject();
        System.out.println(deserializedSingleton == UnexpectedSingleton.getInstance());
        //false
    }
}

因为反序列化操作的{@link ObjectInputStream#readOrdinaryObject(boolean)}里面,最靠下的位置上,有一行if判断,检查有没有readResolve方法,有的话调用一下; {@link SerializableSingleton#readResolve()}里面应该返回单例并通知GC处理掉假冒的实例然后运行会发现结果变回了true

public class SerializableSingleton implements Serializable {
    private static SerializableSingleton ourInstance = new SerializableSingleton();

    public static SerializableSingleton getInstance() {
        return ourInstance;
    }

    private SerializableSingleton() {
        super();
    }

    private Object readResolve(){
        //TODO let the garbage collector take care of the SerializableSingleton impersonator
        return ourInstance;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //write
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempfile2"));
        oos.writeObject(SerializableSingleton.getInstance());
        //read
        File file = new File("tempfile2");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        SerializableSingleton deserializedSingleton = (SerializableSingleton) ois.readObject();
        System.out.println(deserializedSingleton == SerializableSingleton.getInstance());
        //true
    }
}

另外,一般情况下,我们并不能对类的私有字段进行操作,利用反射也不例外。但有时候,例如要序列化时,我们又必须有能力去处理这些字段,可以用AccessibleObject.setAccessible()方法来允许这种访问;而由于反射类中的Field,Method和Constructor继承自AccessibleObject,因此,通过在这些类上调用setAccessible()方法,可以实现对这些字段的操作。

但有的时候这将会成为一个安全隐患,为此,我们可以启用java.security.manager来判断程序是否具有调用setAccessible()的权限。默认情况下,内核API和扩展目录的代码具有该权限,而类路径或通过URLClassLoader加载的程序没有。

package tradiationalsingleton;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;

/**
 * 一般情况下,我们并不能对类的私有字段进行操作,利用反射也不例外,但有时候,例如要序列化时,
 * 我们又必须有能力去处理这些字段,可以用AccessibleObject.setAccessible()方法来允许这种访问,
 * 而由于反射类中的Field,Method和Constructor继承自AccessibleObject,
 * 因此,通过在这些类上调用setAccessible()方法,可以实现对这些字段的操作。
 *
 * 但有的时候这将会成为一个安全隐患,为此,我们可以启用java.security.manager
 * 来判断程序是否具有调用setAccessible()的权限。
 * 默认情况下,内核API和扩展目录的代码具有该权限,而类路径或通过URLClassLoader加载的程序没有。
 */
public class ModifyPrivateDataTest {

    static class A {
        private int data = 0;
        void printData(){
            System.out.println(data);
        }
    }

    public static void main(String[] args) {
        A a1 = new A();
        Field[] fields = a1.getClass().getDeclaredFields();
        AccessibleObject.setAccessible(fields, true);
        try {
            a1.printData();
            fields[0].setInt(a1, 150);
            a1.printData();
        } catch (IllegalAccessException | IllegalArgumentException ex1) {
            ex1.printStackTrace();
        }
    }
}

Enum实现单例

在Java1.5之后,由于枚举类Enum的引入,出现了单例的第三种写法。

单元素枚举类型由于其: 功能完整、使用简洁、无偿地提供了序列化机制、面对复杂的序列化或反射攻击时仍然可以绝对防止多次实例化等优点,被《Effective Java》的作者认为是实现Singleton的最佳方法。
Enum序列化机制简介:在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。

public enum EnumSingleton {
    /**
     * Enum实现单例
     */
    INSTANCE;

    private B myB = null;

    private EnumSingleton(){
        myB = new B();
    }
    public B getB(){
        System.out.println("哔哔哔");
        return myB;
    }
}

全代码git地址:点我点我

猜你喜欢

转载自blog.csdn.net/q2878948/article/details/81324713