java ArrayList的序列化分析

1、緒論

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

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

經過對ArrayList源碼的分析,能夠知道ArrayList的數據存儲都是依賴於elementData數組,它的聲明爲:數組

transient Object[] elementData;
注意transient修飾着elementData這個數組。

一、先看看transient關鍵字的做用

咱們都知道一個對象只要實現了Serilizable接口,這個對象就能夠被序列化,java的這種序列化模式爲開發者提供了不少便利,咱們能夠沒必要關係具體序列化的過程,只要這個類實現了Serilizable接口,這個類的全部屬性和方法都會自動序列化。緩存

      然而在實際開發過程當中,咱們經常會遇到這樣的問題,這個類的有些屬性須要序列化,而其餘屬性不須要被序列化,打個比方,若是一個用戶有一些敏感信息(如密碼,銀行卡號等),爲了安全起見,不但願在網絡操做(主要涉及到序列化操做,本地序列化緩存也適用)中被傳輸,這些信息對應的變量就能夠加上 transient關鍵字。換句話說,這個字段的生命週期僅存於調用者的內存中而不會寫到磁盤裏持久化。安全

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

具體詳見:Java transient關鍵字使用小記app

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

2、序列化工做流程

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

在序列化和反序列化過程當中須要特殊處理的類必須使用下列準確簽名來實現特殊方法:.net

private void writeObject(java.io.ObjectOutputStream out) throws IOException

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException

 

一、對象序列化步驟

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;
    }
}

3、ArrayList解決序列化

一、序列化

從上面序列化的工做流程能夠看出,要想序列化對象,使用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修飾,不能被序列化,可是咱們能夠將它的值取出來,而後將該值寫入輸出流。

// 片斷1 它的功能等價於片斷2
s.writeObject(elementData[i]);  // 傳值時,是將實參elementData[i]賦給s.writeObject()的形參
//  片斷2
Object temp = new Object();     // temp並無被transient修飾
temp = elementData[i];
s.writeObject(temp);

 

二、反序列化

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的,那何時能調用呢?

三、調用

若是一個類不只實現了Serializable接口,並且定義了 readObject(ObjectInputStream in)和 writeObject(ObjectOutputStream out)方法,那麼將按照以下的方式進行序列化和反序列化:

ObjectOutputStream會調用這個類的writeObject方法進行序列化,ObjectInputStream會調用相應的readObject方法進行反序列化。

事情究竟是這樣的嗎?咱們作個小實驗,來驗明正身。 
實驗1:

public class TestSerialization implements Serializable
{
    private transient int    num;

    public int getNum()
    {
        return num;
    }

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

    private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException
    {
        s.defaultWriteObject();
        s.writeObject(num);
        System.out.println("writeObject of "+this.getClass().getName());
    }

    private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException
    {
        s.defaultReadObject();
        num = (Integer) s.readObject();
        System.out.println("readObject of "+this.getClass().getName());
    }

    public static void main(String[] args)
    {
        TestSerialization test = new TestSerialization();
        test.setNum(10);
        System.out.println("序列化以前的值:"+test.getNum());
        // 寫入
        try
        {
            ObjectOutputStream outputStream = new ObjectOutputStream(
                    new FileOutputStream("D:\\test.tmp"));
            outputStream.writeObject(test);
        } catch (FileNotFoundException e)
        {
            e.printStackTrace();
        } catch (IOException e)
        {
            e.printStackTrace();
        }
        // 讀取
        try
        {
            ObjectInputStream oInputStream = new ObjectInputStream(
                    new FileInputStream("D:\\test.tmp"));
            try
            {
                TestSerialization aTest = (TestSerialization) oInputStream.readObject();
                System.out.println("讀取序列化後的值:"+aTest.getNum());
            } catch (ClassNotFoundException e)
            {
                e.printStackTrace();
            }
        } catch (FileNotFoundException e)
        {
            e.printStackTrace();
        } catch (IOException e)
        {
            e.printStackTrace();
        }
    }
}
輸出:

序列化以前的值: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) and writeObject(ObjectOutputStream o)以前呢?
它們的做用以下:
一、It reads and writes all the  non transient fields of the class respectively.
二、 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

4、爲何使用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序列化和反序列話總結

相關文章
相關標籤/搜索