1. 程式人生 > 其它 >Java物件的序列化和反序列化原始碼閱讀

Java物件的序列化和反序列化原始碼閱讀

前言

序列化和反序列化看起來用的不多,但用起來就很關鍵,因為稍一不注意就會出現問題。序列化的應用場景在哪裡?當然是資料儲存和傳輸。比如快取,需要將物件復刻到硬碟儲存,即使斷電也可以重新反序列化恢復。下面簡單理解序列化的用法以及注意事項。

如何序列化

Java中想要序列化一個物件,必須實現Serializable介面。然後就可以持久化和反序列化了。下面是一個簡單用法。

專案測試程式碼: https://github.com/Ryan-Miao/someTest/blob/master/src/main/java/com/test/java/serial/TestSerialize.java

我們給一個測試類:

package com.test.java.serial;

import lombok.Builder;
import lombok.Data;

import java.io.Serializable;

/**
 * @author Ryan Miao
 */
@Data
@Builder
public class Foo implements Serializable {

    private static final String LOGGER = "logger";
    public static final String PUB_STATIC_FINAL = "publicStaticFinal";
    public static String PUB_STATIC;

    public String fa;
    private String fb;
    transient public String ta;
    transient private String tb;
}

然後,測試序列化和反序列的資料是否丟失。

public class TestSerialize {

    private static final String filename = "D:/test.txt";

    @Test
    public void testSer() throws IOException, ClassNotFoundException {
        final Foo foo = Foo.builder()
                .fa("fa")
                .fb("fb")
                .ta("ta")
                .tb("tb")
                .build();

        Foo.PUB_STATIC  = "test";

        ObjectOutputStream os = new ObjectOutputStream(
                new FileOutputStream(filename));
        os.writeObject(foo);
        os.flush();
        os.close();

    }

    @Test
    public void testRead() throws IOException, ClassNotFoundException {
        ObjectInputStream is = new ObjectInputStream(new FileInputStream(filename));
        Foo foo2 = (Foo) is.readObject();
        is.close();

        Assert.assertEquals("fa", foo2.getFa());
        Assert.assertEquals("fb", foo2.getFb());
        Assert.assertEquals(null, foo2.getTa());
        Assert.assertEquals(null, foo2.getTb());

        Assert.assertNull(foo2.PUB_STATIC);
    }
}

顯然,transient修飾的欄位不能被序列化,至於靜態欄位,這裡不做測試,但要清楚。靜態欄位只和class類相關,和例項無關。而序列化是針對例項的,所以無所謂對比內容變化。那麼,靜態欄位反序列化後資料是什麼樣子的呢?當然是類變數本身應該的樣子。如果沒有初始化,則是預設值, 本測試中的結果為null。

為什麼可以序列化

我們只要實現了Serialiable就可以序列化,那麼為什麼呢?檢視ObjectOutputStreamwriteObject方法。

// remaining cases
if (obj instanceof String) {
    writeString((String) obj, unshared);
} else if (cl.isArray()) {
    writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
    writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
    writeOrdinaryObject(obj, desc, unshared);
} else {
    if (extendedDebugInfo) {
        throw new NotSerializableException(
            cl.getName() + "n" + debugInfoStack.toString());
    } else {
        throw new NotSerializableException(cl.getName());
    }
}

顯然,只針對String,Enum以及Serializable做了處理,因此想要序列化必須要實現這個介面。當然,String和Enum也實現了Serializable。

如何自定義序列化,Java基礎類庫中的ArrayList等為什麼用transient還能序列化

簡單的物件,對於不想序列化的欄位,只要宣告為transient就好。而有時候,我想對部分欄位處理後序列化。比如ArrayList中儲存資料的transient Object[] elementData;。我們知道ArrayList是可以序列化的,根源就在於自定義這裡了。下面跟蹤ObjectOutputStream原始碼,知道自定義的執行部分就可以驗證了。

入口: java.io.ObjectOutputStream#writeObject

public final void writeObject(Object obj) throws IOException {
    if (enableOverride) {
        writeObjectOverride(obj);
        return;
    }
    try {
        writeObject0(obj, false);
    } catch (IOException ex) {
        if (depth == 0) {
            writeFatalException(ex);
        }
        throw ex;
    }
}

然後,核心方法

private void writeObject0(Object obj, boolean unshared)
        throws IOException{
    boolean oldMode = bout.setBlockDataMode(false);depth++;
    try {
        //省略若干行
        for (;;) {
            // 省略若干行
            desc = ObjectStreamClass.lookup(cl, true);
            //省略若干行
        }
        //省略若干行
        if (obj instanceof String) {
            writeString((String) obj, unshared);
        } else if (cl.isArray()) {
            writeArray(obj, desc, unshared);
        } else if (obj instanceof Enum) {
            writeEnum((Enum<?>) obj, desc, unshared);
        } else if (obj instanceof Serializable) {
            writeOrdinaryObject(obj, desc, unshared);
        } else {
            //....
        }
    } finally {
        depth--;
        bout.setBlockDataMode(oldMode);
    }
}

這裡,顯然可以看到真正的執行序列化程式碼是writeOrdinaryObject(obj, desc, unshared);。 但直接追蹤進去發現裡面有許多初始化的欄位是在之前做的處理。因此,先賣個關子,看前面初始化的部分,只找到我們想要初始化的欄位即可。

進入desc = ObjectStreamClass.lookup(cl, true);

static ObjectStreamClass lookup(Class<?> cl, boolean all) {
    //省略若干行
    if (entry == null) {
        try {
            entry = new ObjectStreamClass(cl);
        } catch (Throwable th) {
            entry = th;
        }
        //.....
    }
    //省略若干行
}

進入entry = new ObjectStreamClass(cl);這裡就是真正的初始化地方,前面省略的程式碼是快取處理,當然快取使用的ConcurrentHashMap。

private ObjectStreamClass(final Class<?> cl) {
    //省略無數行以及括號
    writeObjectMethod = getPrivateMethod(cl, "writeObject",
                            new Class<?>[] { ObjectOutputStream.class },
                            Void.TYPE);
    readObjectMethod = getPrivateMethod(cl, "readObject",
                            new Class<?>[] { ObjectInputStream.class },
                            Void.TYPE);
    //省略無數行

沒錯,費了這麼大勁就是為了找到這兩個method。通過反射,獲取到目標class的兩個私有方法writeObject, readObject。這兩個就是自定義方法所在。

初始化完畢之後,我們再來繼續序列化的程式碼. 回到剛才的核心方法,找到writeOrdinaryObject(obj, desc, unshared);, 進入,然後,繼續找到writeSerialData(obj, desc);, 到這裡就是真正執行序列化的程式碼了。

private void writeSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
{
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
    for (int i = 0; i < slots.length; i++) {
        ObjectStreamClass slotDesc = slots[i].desc;
        if (slotDesc.hasWriteObjectMethod()) {
            //....
            try {
                curContext = new SerialCallbackContext(obj, slotDesc);
                bout.setBlockDataMode(true);
                slotDesc.invokeWriteObject(obj, this);
                bout.setBlockDataMode(false);
                bout.writeByte(TC_ENDBLOCKDATA);
            } finally {
                //...
            }

            curPut = oldPut;
        } else {
            defaultWriteFields(obj, slotDesc);
        }
    }
}

顯然,判斷writeObject這個method是否初始化了,如果有,則直接呼叫這個方法,沒有則預設處理。到此,跟蹤完畢,我想要自定義序列化只要重寫writeObject, readObject這兩個方法即可。

下面看看ArrayList是怎麼做的

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

因為陣列被設定不允許序列化,先預設序列化其他資訊,然後單獨處理數組裡的內容,挨著寫入元素。然後,對應讀取方法也要改。

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

為什麼要這麼做?因為陣列元素有很多空餘空間,對我們來說不需要序列化。通過這樣自定義,把需要的元素序列化,可以節省空間。

serialVersionUID為什麼有的有,有的沒有,什麼時候用,意義是什麼

以下內容來自: https://www.cnblogs.com/ouym/p/6654798.html

什麼是serialVersionUID ?

serialVersionUID表示:“序列化版本統一識別符號”(serial version universal identifier),簡稱UID

serialVersionUID必須定義成下面這種形式:static final long serialVersionUID = xxxL;

serialVersionUID 用來表明類的不同版本間的相容性。有兩種生成方式: 一個是預設的1L;另一種是根據類名、介面名、成員方法及屬性等來生成一個64位的雜湊欄位 。

為什麼要宣告serialVersionUID

java.io.ObjectOutputStream代表物件輸出流,它的writeObject(Object obj)方法可對引數指定的obj物件進行序列化,把得到的位元組序列寫到一個目標輸出流中。 java.io.ObjectInputStream代表物件輸入流,它的readObject()方法從一個源輸入流中讀取位元組序列,再把它們反序列化為一個物件,並將其返回。

只有實現了Serializable或Externalizable介面的類的物件才能被序列化。

Externalizable介面繼承自Serializable介面,實現Externalizable介面的類完全由自身來控制序列化的行為,而僅實現Serializable介面的類可以採用預設的序列化方式 。 凡是實現Serializable介面的類都有一個表示序列化版本識別符號的靜態變數:private static final long serialVersionUID;

類的serialVersionUID的預設值完全依賴於Java編譯器的實現,對於同一個類,用不同的Java編譯器編譯,有可能會導致不同的serialVersionUID。顯式地定義serialVersionUID有兩種用途:

  1. 在某些場合,希望類的不同版本對序列化相容,因此需要確保類的不同版本具有相同的serialVersionUID;在某些場合,不希望類的不同版本對序列化相容, 因此需要確保類的不同版本具有不同的serialVersionUID。
  2. 當你序列化了一個類例項後,希望更改一個欄位或新增一個欄位,不設定serialVersionUID,所做的任何更改都將導致無法反序化舊有例項,並在反序列化時丟擲一個異常。 如果你添加了serialVersionUID,在反序列舊有例項時,新新增或更改的欄位值將設為初始化值(物件為null,基本型別為相應的初始預設值),欄位被刪除將不設定。

注意事項

  1. 序列化時,只對物件的狀態進行儲存,而不管物件的方法;
  2. 當一個父類實現序列化,子類自動實現序列化,不需要顯式實現Serializable介面;
  3. 當一個物件的例項變數引用其他物件,序列化該物件時也把引用物件進行序列化;
  4. 並非所有的物件都可以序列化,,至於為什麼不可以,有很多原因了,比如:
    1. 安全方面的原因,比如一個物件擁有private,public等field,對於一個要傳輸的物件,比如寫到檔案,或者進行rmi傳輸等等,在序列化進行傳輸的過程中,這個物件的private等域是不受保護的。
    2. 資源分配方面的原因,比如socket,thread類,如果可以序列化,進行傳輸或者儲存,也無法對他們進行重新的資源分 配,而且,也是沒有必要這樣實現。

參考