第20次文章:內部類+單例設計模式

  • 2019 年 10 月 8 日
  • 筆記

本周首先緊接上周沒有寫完的內部類,詳情點擊《第19次文章:類加載器的加密解密+內部類》,再加單例模式的實現。

一、另外兩種內部類的基本用法

(3)方法類基本用法:

方法內部類的地位和方法內的局部變量 的地位類似

因此不能修飾局部變量的修飾符也不能修飾局部內部類,譬如public、private、protected、static、transient等。

-方法內部類只能在聲明的方法內是可見的

因此定義局部內部類之後,想用的話就要在此方法內直接實例化,記住這裡順序不能反了,一定是要先聲明後使用,否則編譯器就會找不到。

-方法內部類不能訪問定義它的方法內的局部變量,除非這個變量被定義為final

本質原因:局部變量和內部類生命周期不一致所致!

-方法內部類只能包含非靜態成員!

(4)匿名內部類基本用法:

-匿名內部類的實例化方式:new SomeInterfaceOrClass(){………};

意思是創造了一個實現(繼承)了SomeInterfaceOrClass的類的對象;

-根據聲明的位置,判斷匿名內部類是成員內部類還是方法內部類。註:一般是方法內部類,這就具備方法內部類的特性。

-三種使用方法:

(1)接口式

(2)繼承式

(3)參數式

對於匿名類,我們利用一段實例進行詳解:

/** * 測試匿名內部類的幾種方式 */public class Demo05 {    public static void main(String[] args) {    Outer05 o5 = new Outer05();    o5.test();  }}  class Outer05{  //參數式匿名內部類  public void test02(Car car) {    car.run();  }    public void test() {    //匿名內部類(接口式),由於本內部類定義在方法中,也是方法內部類    Runnable runnable = new Runnable() {      @Override      public void run() {              }    };          //匿名內部類,繼承式    Car car = new Car() {      @Override      public void run() {        System.out.println("繼承式子類的汽車跑");      }    };    car.run();        //利用參數式    test02(new Car() {      @Override      public void run() {        System.out.println("參數式匿名內部類,車在跑");      }    });  }}  class Car{  public void run () {    System.out.println("汽車跑");  }}

查看一下結果:

tips:

(1)在這段代碼中,我們分別介紹了匿名內部類的三種模式,對於第一種接口式,我們使用Runnable進行創建,然後重新定義其中的run方法,可以在裏面增添我們需要的相關操作。

(2)對於繼承式,我們首先需要創建一個父類Car,然後我們再來利用繼承關係,創建一個匿名的繼承式匿名內部類,在使用繼承式匿名類的時候,我們同樣可以更改父類的原有方法。

(3)利用參數式的匿名內部類,需要根據方法的要求,在傳遞參數的時候,直接創建一個匿名類,並且可以根據我們自己的需求修改其原有方法。

(4)匿名內部類的最大特點就是,我們創建的對象沒有名稱,這也就代表着我們對於此類只能在其創建的時候使用一次。使用結束之後,我們就無法再次利用匿名類了。在實際情況中,也的確有很多對象我們只會使用一次。此時使用匿名類就是一個很好的選擇。

二、設計模式GOF23

1、創建型模式

-單列模式,工廠模式,抽象工廠模式,建造者模式,原型模式

2、結構型模式

-適配器模式,橋接模式,裝飾模式,組合模式,外觀模式,享元模式,代理模式

3、行為型模式

-模板方法模式,命令模式、迭代器模式、觀察者模式、中介者模式、備忘錄模式、解釋器模式、狀態模式、策略模式、職責鏈模式、訪問者模式。

三、單例模式

1、核心作用:

保證一個類只有一個實例,並且提供一個訪問實例的全局訪問點。比如說我們在打開Windows下的資源管理器的時候,無論我們打開多少次,每次打開的對象都會指向同一個資源管理器,但是QQ就不一樣了啊,如果你不斷的點擊QQ的快捷方式,它會不斷的產生新的QQ登錄界面,這就不屬於單例模式。

2、優點:

-由於單例模式只生成一個實例,減少了系統性能開銷,當一個對象的產生需要比較多的資源時,如讀取配置文件、產生其他依賴對象時,則可以通過在應用啟動時直接產生一個單例對象,然後永久駐留內存的方式來解決。

-單例模式可以在系統設計全局的訪問點,優化環共享資源訪問,例如可以設計一個單例類,負責所有數據表的映射處理。

3、常見的5中單例模式實現方式:

(1)主要:

-餓漢式(線程安全,調用效率高。但是,不能延時加載。)

-懶漢式(線程安全,調用效率不高。但是,可以延時加載。)

(2)其他:

-雙重檢測鎖式(由於JVM底層內部模型原因,偶爾會出問題,不建議使用)

-靜態內部類式(線程安全,調用效率高。但是,可以延時加載)

-枚舉單例(線程安全,調用效率高,不能延時加載。並且可以天然的防止反射和反序列化漏洞)

4、單例模式的實現與檢測

我們對5種單例模式都進行了相應的實現。下面我們主要對餓漢式和懶漢式進行一個詳解。

實現的主要思想:由於是屬於單例模式,每次創建或者打開得到的是同一個對象,所以我們首先需要將構造器私有化,不能讓外部的用戶隨意的創建對象。而每次創建對象的時候,我們就需要去查看是否已經有這個類的對象存在了,如果存在的話,就不能再創建新對象了,而是將已經創建好的對象直接返回給用戶。

(1)首先看一下餓漢式:

package com.peng.singleton;/** * 測試餓漢式單例模式 *一旦此類開始初始化,就已經new出一個對象了,但是此對象後續可能沒有使用到。 */public class SingletonDemo01 {    //類初始化時,立即加載這個對象(沒有延時加載的優勢)。加載類時,天然的是線程安全的!  private static SingletonDemo01 singletonDemo01 = new SingletonDemo01();     //私有構造器  private SingletonDemo01() {  }  //方法沒有同步,調用效率高!  public static SingletonDemo01 getInstance() {    return singletonDemo01;  }}

(2)再看一下懶漢式:

package com.peng.singleton;  /** * 測試懶漢式單例模式 */  public class SingletonDemo02 {    //類初始化的時候,不初始化這個對象(延遲加載,真正使用的時候再創建)  private static SingletonDemo02 instance;    //構造器私有化  private SingletonDemo02() {      }    //方法同步,調用效率低  public static synchronized SingletonDemo02 getInstance() {    if (instance == null) {      instance = new SingletonDemo02();    }    return instance;  }}

(3)我們檢測一下最後的效果:

package com.peng.singleton;  /** * 檢測單例模式 */public class Client {  public static void main(String[] args) {    SingletonDemo01 s1 = SingletonDemo01.getInstance();    SingletonDemo01 s2 = SingletonDemo01.getInstance();    System.out.println(s1);    System.out.println(s2);        SingletonDemo02 s3 = SingletonDemo02.getInstance();    SingletonDemo02 s4 = SingletonDemo02.getInstance();      System.out.println(s3 == s4);  }}

結果如下所示:

tips:

1.我們分別創建兩個餓漢式和懶漢式的單例模式,然後對比每種單例模式最後產生的對象是否相同,由最後的結果可以看出,餓漢式的兩個對象s1和s2是相同的,懶漢式的兩個對象s3和s4也是相同的。

2.在SingletonDemo01中,我們在類的設計過程時,為了避免外界對構造器的調用,我們首先就將構造器私有化,然後創建一個靜態對象singletonDemo01,並且將其初始化一個新對象,最後給外界留一個公開的獲取該對象的方法getInstance(),直接將最開始初始化的對象返回給用戶,這樣就保證了用戶無法自己創建該對象,只能獲取該對象,而獲取到的對象一直都是同一個對象。同時我們也可以看到,在加載此類的時候,局部變量singletonDemo01直接被初始化了,所以這種模式不具有延時加載的效果,被稱為餓漢式單例模式。

3.在SingletonDemo02中,思想與SingletonDemo01相似,但是我們注意到兩者之間的區別在於,SingletonDemo01中,一旦加載該類,靜態對象singletonDemo01就會被立即加載,而在SingletonDemo02中,靜態對象instance只有在我們實際調用的時候才會被加載,這就屬於一個延時加載,極大的節約了資源。但是在調用的時候需要考慮多線程的問題,避免重複加載,造成單例模式的失效。所以我們在方法getInstance的前面需要加上synchronized進行方法同步。這樣也就降低了SingletonDemo02的效率。由於SingletonDemo02隻有在需要的時候才去初始化對象,所以我們稱SingletonDemo02為懶漢式。

5、問題:

在java中擁有一種動態機制,反射和序列化,這種動態機制(詳情見:第15次文章:反射+動態編譯+腳本引擎)可以破解上面幾種(不包含枚舉式)單例實現方式。所以針對這種安全漏洞,我們需要設計相應的處理方法來進行解決。我們新建一個懶漢式類SingletonDemo06來進行說明。

(1)對於反射

在反射機制中,主要是通過獲取類的一個class對象,然後通過這個class對象,調用類對象的構造器,創建相應的類。具體如下所示:

/** * 測試反射和反序列化破解單例模式 */public class Client2 {  public static void main(String[] args) throws Exception {    SingletonDemo06 s1 = SingletonDemo06.getInstance();    SingletonDemo06 s2 = SingletonDemo06.getInstance();        System.out.println(s1);    System.out.println(s2);        //通過反射的方式直接調用私有構造器    Class<SingletonDemo06> clazz =  (Class<SingletonDemo06>) Class.forName("com.peng.singleton.SingletonDemo06");        Constructor<SingletonDemo06> c = clazz.getDeclaredConstructor(null);    c.setAccessible(true);    SingletonDemo06 s3 = c.newInstance(null);    SingletonDemo06 s4 = c.newInstance(null);        System.out.println(s3);    System.out.println(s4);  }}

查看一下結果:

tips:

如圖所示,我們通過反射機制,生成了兩個不同的對象s3和s4。這就屬於一個單例的漏洞。針對這個問題的關鍵還是在於懶漢式的構造器。我們的解決方案就可以在構造器中手動拋出異常,針對這個解決方案,我們將SingletonDemo06中的構造器改為:

private SingletonDemo06() {    //破解反射構造多個對象    if(instance != null) {      throw new  RuntimeException();    }  }

改造之後,我們再來查看一下結果:

tips:

當我們再利用反射機制時,一旦檢測到靜態變量instance不是空的時候,就會拋出異常,阻止其繼續創建新對象。

(2)對於序列化

序列化的原理在於將對象轉換為位元組數組進行存儲以及獲取。我們首先對其進行測試

/** * 測試反射和反序列化破解單例模式 */public class Client2 {  public static void main(String[] args) throws Exception {    SingletonDemo06 s1 = SingletonDemo06.getInstance();    SingletonDemo06 s2 = SingletonDemo06.getInstance();        System.out.println(s1);    System.out.println(s2);        //通過反序列化的方式構造多個對象    FileOutputStream fos = new FileOutputStream("G:/java學習/test/a.txt");    ObjectOutputStream oos = new ObjectOutputStream(fos);    oos.writeObject(s1);    oos.close();    fos.close();        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("G:/java學習/test/a.txt"));    SingletonDemo06 s5 = (SingletonDemo06) ois.readObject();    System.out.println(s5);    }}

查看一下結果:

tips:

如圖所示,對於同一個對象s1,經過序列化的處理之後,獲取得到的對象成為了一個新對象。所以此處也破解了單例模式的實現。針對序列化的本質核心,我們在SingletonDemo06中過定義readResolve()防止獲得不同對象。反序列化時,如果對象所在類定義了readResolve()(實際是一種回調),定義返回哪個對象。在SingletonDemo06中定義的readResolve()方法如下所示:

//反序列化時,如果定義了readResolve()則直接返回此方法指定的對象。而不需要單獨再創建新對象!  private Object readResolve() throws ObjectStreamException{    return instance;  }

修改之後,我們再來查看最後的結果:

6、如何選用

在上面的講解中,我們主要講解了餓漢式和懶漢式的細節,其餘的三種由於和兩種主要的單例模式比較類似,枚舉式較為特殊,下面直接給出5種模式的對比,便於我們使用時候的選擇:

-單例對象佔用資源少,不需要延時加載:枚舉式好於餓漢式

-單例對象佔用資源大,需要延時加載:靜態內部類式好於懶漢式