歡迎光臨
每天分享高質量文章

深入理解單例樣式 ( 上 )

(點選上方公眾號,可快速關註)


來源:拿筆小星_ ,

blog.csdn.net/u013096088/article/details/81161084

最近在閱讀《Effective Java 》這本書,第3個條款專門提到了單例屬性,並給出了使用單例的最佳實踐建議。讓我對這個單例樣式(原本我以為是設計樣式中最簡單的一種)有了更深的認識。

單例樣式

單例樣式(Singleton Pattern)是 Java 中最簡單的設計樣式之一。這種型別的設計樣式屬於建立型樣式,它提供了一種建立物件的最佳方式。

在應用這個樣式時,單例物件的類必須保證只有一個實體存在。許多時候整個系統只需要擁有一個的全域性物件,這樣有利於我們協調系統整體的行為。

單例的特點


  1. 單例類只能有一個實體。

  2. 單例類必須自己建立自己的唯一實體。

  3. 單例類必須給所有其他物件提供這一實體。


單例樣式的7種寫法

單例樣式的寫法很多,涉及到了執行緒安全和效能問題。在這裡我不重覆介紹。這篇《單例樣式的七種寫法》寫得很詳細,博主也給出了每一種寫法的優缺點。

http://www.hollischuang.com/archives/205

但是,單例樣式真的能夠實現實體的唯一性嗎?答案是否定的。

如何破壞單例

反射

有兩種常見的方式來實現單例。他們的做法都是將構造方法設為私有,並匯出一個公有的靜態成員來提供對唯一實例的訪問。在第1種方式中,成員是個final欄位:

// Singleton with public final field

public class Elvis {

    public static final Elvis INSTANCE = new Elvis();

    private Elvis() { … }

    public void leaveTheBuilding() { … }

}

只呼叫私有建構式一次,以初始化公共靜態final欄位elvi.instance。不提供公有的或者受保護的建構式保證了全域性唯一性:當Elvis類初始化的時候,僅僅只會有一個Elvis實體存在——不多也不少 。無論客戶端怎麼做都無法改變這一點,只不過我還是要警告一下 :授權的客戶端可以透過反射來呼叫私有構造方法,藉助於AccessibleObject.setAccessible方法即可做到 。如果需要防範這種攻擊,請修改建構式,使其在被要求建立第二個實體時丟擲異常。

測試程式碼:

public class TestSingleton {

 

    /**

     * 透過反射破壞單例

     */

    @Test

    public void testReflection() throws Exception {

        /**

         * 驗證單例有效性

         */

        Elvis elvis1 = Elvis.INSTANCE;

        Elvis elvis2 = Elvis.INSTANCE;

 

        System.out.println(“elvis1 == elvis2 ? ===>” + (elvis1 == elvis2));

        System.err.println(“—————–“);

 

        /**

         * 反射呼叫構造方法

         */

        Class clazz = Elvis.class;

        Constructor cons = clazz.getDeclaredConstructor(null); 

        cons.setAccessible(true);

 

        Elvis elvis3 = (Elvis) cons.newInstance(null);

 

        System.out.println(“elvis1 == elvis3 ? ===> “

            + (elvis1 == elvis3));

    }

}

執行結果:

Elvis Constructor is invoked!

elvis1 == elvis2 ? ===> true

elvis1 == elvis3 ? ===> false

—————–

Elvis Constructor is invoked!

結論:

反射是可以破壞單例屬性的。因為我們透過反射把它的建構式設成可訪問的,然後去生成一個新的物件。

改進版的單例寫法:

public class Elvis {

    public static final Elvis INSTANCE = new Elvis();

 

    private Elvis() { 

        System.err.println(“Elvis Constructor is invoked!”);

        if (INSTANCE != null) {

            System.err.println(“實體已存在,無法初始化!”);

            throw new UnsupportedOperationException(“實體已存在,無法初始化!”);

        }

    } 

}

結果:

Elvis Constructor is invoked!

elvis1 == elvis2 ? ===> true

—————–

Elvis Constructor is invoked!

實體已存在,無法初始化!

第2種實現單例樣式的方法是,提供一個公有的靜態工廠方法:

// Singleton with static factory

public class Elvis {

    private static final Elvis INSTANCE = new Elvis();

    private Elvis() { … }

    public static Elvis getInstance() { return INSTANCE; }

    public void leaveTheBuilding() { … }

}

所有呼叫Elvis類的getInstance方法,傳回相同的物件取用,並且不會有其它的Elvis物件被建立。但同樣有上面第1個方法提到的反射破壞單例屬性的問題存在。

序列化和反序列化

如果對上述2種方式實現的單例類進行序列化,反序列化得到的物件是否是同一個物件呢?答案是否定的。

看下麵的測試程式碼:

單例類:

public class Elvis implements Serializable {

    public static final Elvis INSTANCE = new Elvis();

 

    private Elvis() { 

        System.err.println(“Elvis Constructor is invoked!”);

    }

}

測試程式碼:

/**

 * 序列化對單例屬性的影響

 * @throws Exception 

 */

@Test

public void testSerialization() throws Exception {

    Elvis elvis1 = Elvis.INSTANCE;

    FileOutputStream fos = new FileOutputStream(“a.txt”);

    ObjectOutputStream oos = new ObjectOutputStream(fos);

    oos.writeObject(elvis1);

    oos.flush();

    oos.close();

 

    Elvis elvis2 = null;

    FileInputStream fis = new FileInputStream(“a.txt”);

    ObjectInputStream ois = new ObjectInputStream(fis);

    elvis2 = (Elvis) ois.readObject();

 

    System.out.println(“elvis1 == elvis2 ? ===>” + (elvis1 == elvis2));

}

結果是:

Elvis Constructor is invoked! 

elvis1 == elvis2 ? ===>false

說明:

透過對序列化後的Elvis 進行反序列化得到的物件是一個新的物件,這就破壞了Elvis 的單例性。

【關於投稿】


如果大家有原創好文投稿,請直接給公號傳送留言。


① 留言格式:
【投稿】+《 文章標題》+ 文章連結

② 示例:
【投稿】《不要自稱是程式員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/

③ 最後請附上您的個人簡介哈~



看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂