java ArrayList的序列化分析

一、緒論

JAVA 序列化就是將 JAVA 對象以一種形式保持,比如存放到硬盤,或是用於傳輸。反序列化是序列化的一個逆過程。

JAVA 規定被序列化的對象必須實現 java.io.Serializable 這個接口,而我們分析的 ArrayList 同樣實現了該接口。

通過對 ArrayList 源碼的分析,可以知道 ArrayList 的數據存儲依賴於 elementData 數組,它的聲明爲:
transient Object[] elementData;
注意 transient 修飾着 elementData 這個數組。

先看看 transient 關鍵字的作用

我們都知道一個對象只要實現了 Serializable 接口,這個對象就可以被序列化,java 的這種序列化模式爲開發者提供了很多便利,我們可以不必關係具體序列化的過程,只要這個類實現了 Serializable 接口,這個類的所有屬性都會自動序列化。

然而在實際開發過程中,我們常常會遇到這樣的問題,類的有些屬性需要序列化,有些屬性不需要序列化,打個比方,例如用戶的敏感信息(如密碼,銀行卡號等),不希望在網絡(主要涉及到序列化操作,本地序列化緩存也適用)中被傳輸,這些信息對應的變量就可以加上 transient 關鍵字。

總之,java 的 transient 關鍵字爲我們提供了便利,你只需要實現 Serializable 接口,將不需要序列化的屬性前添加關鍵字 transient,對象序列化的時候,這個屬性就不會序列化到指定的目的地中。

既然 elementData 被 transient 修飾,按理來說,它不能被序列化的,那麼 ArrayList 又是如何解決序列化這個問題的呢?

二、序列化流程

類通過實現 java.io.Serializable 接口可以啓用其序列化功能。要序列化一個對象,必須與一定的對象輸出/輸入流聯繫起來,通過對象輸出流將對象狀態保存下來,再通過對象輸入流將對象狀態恢復。

在序列化和反序列化過程中需要特殊處理的類,必須嚴格遵從下面的寫法(私有,void返回值類型,函數名,參數):

private void writeObject(java.io.ObjectOutputStream out)

private void readObject(java.io.ObjectInputStream in)

對象序列化步驟

a) 寫入

  • 首先創建一個 OutputStream 輸出流;
  • 然後創建一個 ObjectOutputStream 輸出流,並傳入 OutputStream 輸出流對象;
  • 最後調用 ObjectOutputStream 對象的 writeObject() 方法將對象狀態信息寫入 OutputStream。

b) 讀取

  • 首先創建一個 InputStream 輸入流;
  • 然後創建一個 ObjectInputStream 輸入流,並傳入 InputStream 輸入流對象;
  • 最後調用 ObjectInputStream 對象的 readObject() 方法從 InputStream 中讀取對象狀態信息。

舉例說明:

public class Box implements Serializable {
    private static final long serialVersionUID = -3450064362986273896L;
    
    private int width;
    private int height;
    
    public static void main(String[] args) {
        Box myBox = new Box();
        myBox.setWidth(50);
        myBox.setHeight(30);
        try {
            FileOutputStream fs = new FileOutputStream("F:\\foo.ser");
            ObjectOutputStream os = new ObjectOutputStream(fs);
            os.writeObject(myBox);
            os.close();
            FileInputStream fi = new FileInputStream("F:\\foo.ser");
            ObjectInputStream oi = new ObjectInputStream(fi);
            Box box = (Box)oi.readObject();
            oi.close();
            System.out.println(box.height+","+box.width);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public int getWidth() {
        return width;
    }
    public void setWidth(int width) {
        this.width = width;
    }
    public int getHeight() {
        return height;
    }
    public void setHeight(int height) {
        this.height = height;
    }
}

三、ArrayList解決序列化

1、序列化

從上面序列化的工作流程可以看出,要想序列化對象,使用 ObjectOutputStream 對象輸出流的 writeObject() 方法寫入對象狀態信息,即可使用 readObject() 方法讀取信息。

那是不是可以在 ArrayList 中調用 ObjectOutputStream 對象的 writeObject() 方法將 elementData 的值寫入輸出流呢?

見源碼:

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);

        // Write out all elements in the proper order.
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

雖然 elementData 被 transient 修飾,不能被序列化,但是我們可以將它的值取出來,然後將該值寫入輸出流。

2、反序列化

ArrayList 的反序列化處理原理同上,見源碼:

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;

        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in capacity
        s.readInt(); // ignored

        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            ensureCapacityInternal(size);

            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }

從上面源碼又引出另外一個問題,這些方法都定義爲 private 的,那什麼時候能調用呢?

3、調用

如果一個類不僅實現了 Serializable 接口,而且定義了 readObject(ObjectInputStream in) 和 writeObject(ObjectOutputStream out) 方法,那麼將按照如下的方式進行序列化和反序列化:
ObjectOutputStream 會調用這個類的 writeObject 方法進行序列化,ObjectInputStream 會調用相應的 readObject 方法進行反序列化。

事情到底是這樣的嗎?我們做個小實驗,來驗明正身。
實驗1:

import java.io.*;

public class TestSerialization implements Serializable{
	private static final long serialVersionUID = 5732067711721143635L;
	private transient int num;

	public int getNum(){
		return num;
	}

	public void setNum(int num){
		this.num = num;
	}

	private void writeObject(ObjectOutputStream s){
		try {
			s.defaultWriteObject();
			s.writeObject(num);
			System.out.println("writeObject of "+this.getClass().getName());
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	private void readObject(ObjectInputStream s){
		try {
			s.defaultReadObject();
			num = (Integer) s.readObject();
			System.out.println("readObject of "+this.getClass().getName());
		} catch (ClassNotFoundException | IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public static void main(String[] args) throws IOException, ClassNotFoundException{
		TestSerialization test = new TestSerialization();
		test.setNum(10);
		System.out.println("序列化之前的值:"+test.getNum());
		// 寫入
		ObjectOutputStream outputStream = new ObjectOutputStream(
				new FileOutputStream("D:\\test.tmp"));
		outputStream.writeObject(test);
		outputStream.close();
		
		// 讀取
		ObjectInputStream inputStream = new ObjectInputStream(
				new FileInputStream("D:\\test.tmp"));
		TestSerialization aTest = (TestSerialization) inputStream.readObject();
		inputStream.close();
		
		System.out.println("讀取序列化後的值:"+aTest.getNum());
	}
}

輸出:
序列化之前的值:10
writeObject of TestSerialization
readObject of TestSerialization
讀取序列化後的值:10

實驗結果證明,事實確實是如此:
ObjectOutputStream 會調用這個類的 writeObject 方法進行序列化,ObjectInputStream 會調用相應的readObject 方法進行反序列化。
那麼 ObjectOutputStream 又是如何知道一個類是否實現了 writeObject 方法呢?又是如何自動調用該類的 writeObject 方法呢?
答案是:通過反射機制實現的。
部分解答:
ObjectOutputStream 的 writeObject 又做了哪些事情。它會根據傳進來的 ArrayList 對象得到 Class,然後再包裝成 ObjectStreamClass,在 writeSerialData 方法裏,會調用 ObjectStreamClass 的 invokeWriteObject 方法,最重要的代碼如下:

writeObjectMethod.invoke(obj, new Object[]{ out });

實例變量 writeObjectMethod 的賦值方式如下:

writeObjectMethod = getPrivateMethod(cl, "writeObject",
		new Class<?>[] { ObjectOutputStream.class },
		Void.TYPE);
    private static Method getPrivateMethod(Class<?> cl, String name,
                                           Class<?>[] argTypes,
                                           Class<?> returnType)
    {
        try {
            Method meth = cl.getDeclaredMethod(name, argTypes);
            // 通過反射訪問對象的 private 方法
            meth.setAccessible(true);
            int mods = meth.getModifiers();
            return ((meth.getReturnType() == returnType) &&
                    ((mods & Modifier.STATIC) == 0) &&
                    ((mods & Modifier.PRIVATE) != 0)) ? meth : null;
        } catch (NoSuchMethodException ex) {
            return null;
        }
    }

在做實驗時,我們發現一個問題,那就是爲什麼需要 s.defaultWriteObject(); 和 s.defaultReadObject(); 語句在 readObject(ObjectInputStream o) 和 writeObject(ObjectOutputStream o) 之前呢?
它們的作用如下:

  1. It reads and writes all the non transient fields of the class
    respectively.
  2. These methods also helps in backward and future compatibility. If in
    future you add some non-transient field to the class and you are
    trying to deserialize it by the older version of class then the
    defaultReadObject() method will neglect the newly added field,
    similarly if you deserialize the old serialized object by the new
    version then the new non transient field will take default value
    from JVM.

四、爲什麼使用 transient 修飾 elementData?

既然要將 ArrayList 的字段序列化(即將 elementData 序列化),那爲什麼又要用 transient 修飾 elementData 呢?

回想 ArrayList 的自動擴容機制,elementData 數組相當於容器,當容器不足時就會再擴充容量,但是容器的容量往往都是大於或者等於 ArrayList 所存元素的個數。

比如,現在實際有了 8 個元素,那麼 elementData 數組的容量可能是 8x1.5=12,如果直接序列化 elementData 數組,那麼就會浪費 4 個元素的空間,特別是當元素個數非常多時,這種浪費是非常不合算的。

所以 ArrayList 的設計者將 elementData 設計爲 transient,然後在 writeObject 方法中手動將其序列化,只序列化實際存儲的那些元素,而不是整個數組。

見源碼:

// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
    s.writeObject(elementData[i]);
}

從源碼中,可以觀察到循環時是使用 i<size 而不是 i<elementData.length,說明序列化時,只需序列化實際存儲的元素,而不是整個數組。
參考:
java.io.Serializable淺析
java serializable深入瞭解
ArrayList源碼分析——如何實現Serializable
java序列化和反序列話總結

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章