設計模式:單例模式的使用和實現(JAVA)

長不大的學生發表於2021-08-19

單例模式的使用

jdk和Spring都有實現單例模式,這裡舉的例子是JDK中Runtime這個類

Runtime的使用

通過Runtime類可以獲取JVM堆記憶體的資訊,還可以呼叫它的方法進行GC。

public class Test {
    public static void main(String[] args) throws Exception {

        Runtime runtime = Runtime.getRuntime();
        runtime.gc();
        //jvm的堆記憶體總量
        System.out.println("堆記憶體總量" + runtime.totalMemory()/1024/1024 + "MB");
        //jvm檢視使用的最大堆記憶體
        System.out.println("最大堆記憶體" + runtime.maxMemory()/1024/1024 + "MB");
        //jvm剩餘可用的記憶體
        System.out.println("可用的記憶體" +runtime.freeMemory()/1024/1024 + "MB");

        Runtime runtime1 = Runtime.getRuntime();

        System.out.println(runtime == runtime1);
    }
}

這裡建立了兩個物件,通過等於號判斷,兩個引用來自同一個物件,確實是單例模式

 

 

Runtime的定義

這個類是介紹是:每一個Java應用有一個Runtime的例項,可以獲取應用執行時的環境屬性,當前的例項通過

getRuntime方法獲取 。應用程式不能建立這個類的例項。

這差不多包含了單例類的定義,然後看一下這個類的內部實現

 

 很明顯是一個標準的單例模式的(餓漢)實現,首先使用static修飾例項物件,所以類載入的時候就會建立例項,然後呼叫方法返回這個例項,使用private修飾建構函式。

 

反射破壞單例模式

Runtime類將建構函式私有化,就是不想讓人建立它的例項,但是我們卻可以使用反射來建立物件

public class Test {
    public static void main(String[] args) throws Exception {

            Class<?> clazz = Runtime.class;
        Constructor constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
        Object o1 = constructor.newInstance();
        Object o2 = Runtime.getRuntime();
            System.out.println(o1.getClass().getSimpleName());
            System.out.println(o2.getClass().getSimpleName());
            System.out.println(o1 == o2);

    }
}

通過執行結果可以看到,已經成功的建立了兩個Runtime物件

 

至於破壞Runtime類的單例有什麼壞處我也不知道,畢竟我是不會用反射去破壞它的,總之應該是有壞處的,下面看一下不能被反射破壞的單例模式實現

 

 

單例模式的實現

列舉類實現

使用列舉實現是因為JDK底層保護我們的列舉類不被反射,就解決了單例被反射破壞的問題

EnumSingleton.java

在列舉類中放了一個內部類(其實不放內部類也行)

public enum EnumSingleton {

    INSTANCE;
    class MyRuntime{

        public void hello(){
            System.out.println("hello");
        }
    }

    private MyRuntime myRuntime;

    EnumSingleton(){
        myRuntime  = new MyRuntime();
    }

    public MyRuntime getData(){
        return myRuntime;
    }

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

下面測試一下這個單例 

public class Test {
    public static void main(String[] args) throws Exception {

        EnumSingleton.MyRuntime myRuntime = EnumSingleton.INSTANCE.getData();
        myRuntime.hello();
        EnumSingleton.MyRuntime myRuntime1 = EnumSingleton.getInstance().getData();
        System.out.println(myRuntime == myRuntime1);
    }
}

結果顯而易見,單例模式已經成功實現

 

 

 至於使用反射測試列舉類,可以直接看一下JDK對列舉類的一個保護

使用反射建立物件,即呼叫Construct類的newInstance方法,這個方法裡面已經定義了列舉物件不能被建立

 

 使用列舉實現單例的壞處有

  • 因為很少使用列舉類,所以用列舉建立單例感覺挺奇怪的。
  • 雖然它可以防止被反射破壞,但是它確實複雜。

 

像上面Runtime類那樣的單例實現就差不多了,有一個缺點是,Runtime在類載入的時候就建立物件了

如果有很多類似的單例實現,在類載入時就建立了很多不需要的物件,會很佔用資源

下面寫一個懶漢式靜態內部類單例實現(呼叫時才建立物件)

public class LazyInnerClassSingleton {

    static {
        System.out.println("載入靜態程式碼塊");
    }

    private LazyInnerClassSingleton(){

        System.out.println("建立物件成功");

    }

    public static void hello(){
        System.out.println("hello");
    }
/*
在呼叫getInstance方法時InnerLazy類被載入的才會初始化物件
 */
    public static LazyInnerClassSingleton getInstance(){
        return InnerLazy.LAZY;
    }

    private static class InnerLazy{

        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();

    }
}

這種實現的要點在與

  • 外部類構造方法私有化,無法建立外部類
  • 內部類的靜態變數LAZY一直到呼叫外部類的getInstance方法時才會被載入,然後LAZY物件才會被建立,實現了懶載入
  • 注意內部類只是提供例項的一個工具,這裡的單例物件是外部類

測試一下是不是真的

public class Test {

    public static void main(String[] args) throws Exception {

        LazyInnerClassSingleton.hello();
        System.out.println("開始建立物件例項");
        LazyInnerClassSingleton.getInstance();
    }
}

由執行結果看到,它只有在呼叫getInstance方法時才會建立物件,在載入外部類時是不會載入內部類的

 

 為了讓它不被反射破壞,在構造方法上多加一個判斷

 

無論是使用new關鍵字還是反射,都會呼叫類的構造方法,所以外部類使用這兩種方式字建立例項,不然就會把異常丟擲
因為if語句永遠為true,雖然在執行if語句之前,InnerLazy.LAZY為null,但是隻要使用了這個變數,就會去載入內部類
載入完內部類,InnerLazy.LAZY就不為null,於是丟擲異常

相關文章