java 序列化

一、緒論

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

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

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

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

1、先看看transient關鍵字的作用

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

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

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

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

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

二、序列化工作流程

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

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

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

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

 

1、對象序列化步驟

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修飾,不能被序列化,但是我們可以將它的值取出來,然後將該值寫入輸出流。

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

 

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:

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) andwriteObject(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,說明序列化時,只需實際存儲的那些元素,而不是整個數組。

參考:

 

 

 

1、java.io.Serializable淺析

2、java serializable深入瞭解

3、ArrayList源碼分析——如何實現Serializable

4、java序列化和反序列話總結

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