1. 程式人生 > >java設計模式之單例模式(列舉、靜態內部類)

java設計模式之單例模式(列舉、靜態內部類)

1、靜態內部類

public class InnerClassSingleton implements Serializable {
    
    //無參建構函式
    private InnerClassSingleton(){};
    
    public static final InnerClassSingleton getInstance(){
        return InnerClassHelper.INSTANCE;
    }
    
    //內部類
    private static class InnerClassHelper{
        private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
    }
}

它的原理是利用了類載入機制。

1.1、但是它可以被反射破壞
	Class clazz = InnerClassSingleton.class;
        Constructor c = clazz.getDeclaredConstructor(null);
        c.setAccessible(true);
        Object o1 = c.newInstance();

        Object o2 = InnerClassSingleton.getInstance();

執行這段程式碼會發現o1<>o2,這就破壞了單例。
而反射的newInstance()的底層用的是下面這行程式碼實現的,它就是罪魁禍首。

UnsafeFieldAccessorImpl.unsafe.allocateInstance(class)

我們知道new建立物件時會被編譯成3條指令:

  • 1、根據型別分配一塊記憶體區域
  • 2、把第一條指令返回的記憶體地址壓入運算元棧頂
  • 3、呼叫類的建構函式

而Unsafe.allocateInstance()方法值做了第一步和第二步,即分配記憶體空間,返回記憶體地址,沒有做第三步呼叫建構函式。所以Unsafe.allocateInstance()方法建立的物件都是隻有初始值,沒有預設值也沒有建構函式設定的值,因為它完全沒有使用new機制,繞過了建構函式直接操作記憶體建立了物件,而單例是通過私有化建構函式來保證的,這就使得單例失敗

1.2、還可以被反序列化破壞
InnerClassSingleton o1 = null;
InnerClassSingleton o2 = InnerClassSingleton.getInstance();

FileOutputStream fos = new FileOutputStream("InnerClassSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(o2);
oos.flush();
oos.close();

FileInputStream fis = new FileInputStream("InnerClassSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
o1 = (InnerClassSingleton) ois.readObject();
ois.close();

System.out.println(o1);
System.out.println(o2);

執行完這段程式碼我們又會發現o1<>o2,可見通過反序列化,成功破壞了單例,建立了2個物件。
那麼如何避免這種情況發生呢?很簡單,只要在程式碼中新增:

public class InnerClassSingleton implements Serializable {
	....省略重複程式碼
	private Object readResolve(){
		return INSTANCE;
	}
}

這時候我們可以再執行一下上面反序列化的方法,會很神奇的發現o1==o2,那這是為什麼呢?我們一起來看下ois.readObject()的原始碼:

private Object readObject0(boolean unshared) throws IOException {
	...省略
	case TC_OBJECT:
	  return checkResolve(readOrdinaryObject(unshared));
}
-------------------------------------------------------------------
private Object readOrdinaryObject(boolean unshared){
	if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();

        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        Object obj;
        try {
	//重點!!!
	//首先isInstantiable()判斷是否可以初始化
	//如果為true,則呼叫newInstance()方法建立物件,這時建立的物件是不走建構函式的,是一個新的物件
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);
	
	//重點!!!
	//hasReadResolveMethod()會去判斷,我們的InnerClassSingleton物件中是否有readResolve()方法
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
	//如果為true,則執行readResolve()方法,而我們在自己的readResolve()方法中 直接retrun INSTANCE,所以還是返回的同一個物件,保證了單例
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
}

最後總結一下靜態內部類寫法:

  • 優點:不用synchronized,效能好;簡單
  • 缺點:無法避免被反射、反序列化破壞

2、列舉

public enum EnumSingleton {
    
    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

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

反編譯這段程式碼,得到:

	static
        {
            INSTANCE = new EnumSingleton("INSTANCE",0);
            $VALUE = (new EnumSingleton[] {
                    INSTANCE
            });
        }

顯然這是一種餓漢式的寫法,用static程式碼塊來保證單例(在類載入的時候就初始化了)。

2.1、可以避免被反射破壞
//反射
Class clazz = EnumSingleton.class;
//拿到建構函式
Constructor c = clazz.getDeclaredConstructor(String.class, int.class);
c.setAccessible(true);
EnumSingleton instance1 = (EnumSingleton)c.newInstance("smart", 111);
-----------------------------------------------------------------------------------------
public T newInstance(Object ... initargs){
	if ((clazz.getModifiers() & Modifier.ENUM) != 0)
   	   throw new IllegalArgumentException("Cannot reflectively create enum objects");
} 

可以看到,在newInstance()方法中,做了型別判斷,如果是列舉型別,直接丟擲異常。也就是說從jdk層面保證了列舉不能被反射。

2.2、可以避免被反序列化破壞

Java規範中規定,每一個列舉型別極其定義的列舉變數在JVM中都是唯一的,在序列化的時候Java僅僅是將列舉物件的name屬性輸出到結果中,反序列化的時候則是通過 java.lang.Enum 的 valueOf() 方法來根據名字查詢列舉物件。

private Object readObject0(boolean unshared) throws IOException {
	...省略
	case TC_ENUM:
	  return checkResolve(readEnum(unshared));
}
-------------------------------------------------------------------
private Object readEnum(boolean unshared){
	...省略
	String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
		//重點!!!
		//通過valueO方法獲取Enum,引數為class和name
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }
}

所以序列化的時候只將 INSTANCE 這個名稱輸出,反序列化的時候再通過這個名稱,查詢對應的列舉型別,因此反序列化後的例項也會和之前被序列化的