1. 程式人生 > >驀然回頭-單例模式篇章三

驀然回頭-單例模式篇章三

單例模式引發相關整理

如何破壞單例模式

示例:

/**
 * 如果破壞單例模式
 *
 * @author sunyang
 * @date 2018/11/13 20:14
 */
public class Singleton7 {
    private Singleton7(){
        System.out.println("Singleton7");
    }
    private static final class SingletonHolder{
        SingletonHolder(){
            System.out.println("SingletonHolder"
); } private static final Singleton7 INSTANCE = new Singleton7(); } public static Singleton7 getInstance(){ return SingletonHolder.INSTANCE; } }

測試得出結果

/**
 * @author sunyang
 * @date 2018/11/13 20:18
 */
public class Test {

    /**
     * ----------------------start-------------
     *Singleton7
     * -----------------------end---------------
     *
     */
public static void main(String[] args) { System.out.println("----------------------start-------------"); Singleton7.getInstance(); System.out.println("-----------------------end---------------"); } }

上圖的單例,最主要的一步是將構造方法私有化,從而外界無法new物件。但是java的反射可以強制訪問private修飾的變數,方法和建構函式,如圖

/**
 * @author sunyang
 * @date 2018/11/13 20:31
 */
public class ReflectTest {

    /**
     * Singleton7
     * Singleton7
     * 反射獲取構造器和直接獲取構造器,比較是否同一個:false
     */

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            Class<Singleton7> singleton7Class = Singleton7.class;
        Constructor<Singleton7> constructor = singleton7Class.getDeclaredConstructor();
        constructor.setAccessible(true);
        //反射獲取建構函式
        Singleton7 singleton1 = constructor.newInstance();

        Singleton7 singleton2 = Singleton7.getInstance();

        System.out.println("反射獲取構造器和直接獲取構造器,比較是否同一個:" + (singleton1 == singleton2));

    }
}
聯想

java中的四種建立物件的方式

typeexplan
new 需要呼叫建構函式
reflect 需要呼叫建構函式,免疫一切訪問許可權的限制public,private等
clone 需要實現Cloneable介面,分淺複製,深層
Serializable 1.將物件儲存在硬碟中 2.通過網路傳輸物件,需要實現Serializable

單例模式,是不能抵抗反射,clone,序列化的破壞的。

如何保護單例模式

思路:

對於clone和序列化,可以在設計的過程中不直接或間接的去實現Cloneable和Serializable介面即可。

對於反射,可以通過在呼叫第二次建構函式的方式進行避免。

嘗試解決程式碼:

/**
 * 保護單例模式測試demo
 *
 * @author sunyang
 * @date 2018/11/14 10:32
 */
public class ReflectTestProtect {
    public static void main(String[] args) {
        try {
            Class<Singleton7Protect> clazz = Singleton7Protect.class;
            Constructor<Singleton7Protect> constructor = clazz.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            Singleton7Protect singleton1 = constructor.newInstance();
            Singleton7Protect singleton2 = Singleton7Protect.getInstance();
            System.out.println("保護單例模式:"+ (singleton1 == singleton2));
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

結果圖

推薦新寫法

jdk1.5以後,實現單例新寫法,只需要編寫一個包含單個元素的列舉型別。

分析

列舉類能夠實現介面,但不能繼承類,列舉類使用enum定義後再編譯時就預設繼承了java.lang.Enum類,而不是普通的繼承Object類。

列舉類會預設實現Serializable和Comparable兩個介面,且採用enum聲明後,該類會被編譯器加上final宣告,故該類是無法繼承的。列舉類的內部定義的列舉值就是該類的例項。除此之外,列舉類和普通類一致。因此可以利用列舉類來實現一個單例模式。直觀圖如下:

推薦寫法程式碼:

/**
 * 新寫法:列舉類來實現一個單例,
 * 來測試破壞,是否會成功?
 *
 * @author sunyang
 * @date 2018/11/14 11:22
 */
public enum  Singleton8 {
    INSTANCE;

    public static Singleton8 getInstance(){
        return INSTANCE;
    }
}

測試是否反射破壞程式碼:

/**
 * 列舉測試反射是否能破壞單例
 * 結論:可以防止單例模式被侵犯
 * @author sunyang
 * @date 2018/11/14 11:26
 */
public class EnumTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<Singleton8> clazz = Singleton8.class;
        Constructor<Singleton8> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        constructor.newInstance();
    }
}

結果圖:

結論:單元素的列舉型別已經成為實現單例模式的最佳方法。

原始碼分析

java.lang.reflect.Constructor#newInstance

@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;
    }

擴充套件:測試對列舉類進行序列化操作程式碼:

/**
 * 測試列舉類是否能防止序列化破壞單例模式
 * 結果:可以防止序列化破壞單例模式
 *
 * @author sunyang
 * @date 2018/11/14 11:52
 */
public class SerializableTest {

    public static void main(String[] args) throws Exception{
        File objectFile = new File("Singleton8.javaObject");
        Singleton8 instance1 = Singleton8.INSTANCE;
        Singleton8 instance2 = null;
        ObjectOutputStream oos = null;
        ObjectInputStream ois = null;
        try {
            //序列化到本地
            oos = new ObjectOutputStream(new FileOutputStream(objectFile));
            oos.writeObject(instance1);
            oos.flush();
            //反序列化到記憶體
            ois = new ObjectInputStream(new FileInputStream(objectFile));
            instance2= (Singleton8) ois.readObject();
        }catch (Exception e){

        }finally {
            oos.close();
            ois.close();
        }

        //true,說明兩者引用著同一個物件
        System.out.println(Objects.equals(instance1, instance2));

    }
}

結論圖