Java物件的序列化和反序列化原始碼閱讀
前言
序列化和反序列化看起來用的不多,但用起來就很關鍵,因為稍一不注意就會出現問題。序列化的應用場景在哪裡?當然是資料儲存和傳輸。比如快取,需要將物件復刻到硬碟儲存,即使斷電也可以重新反序列化恢復。下面簡單理解序列化的用法以及注意事項。
如何序列化
Java中想要序列化一個物件,必須實現Serializable
介面。然後就可以持久化和反序列化了。下面是一個簡單用法。
我們給一個測試類:
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
就可以序列化,那麼為什麼呢?檢視ObjectOutputStream
的writeObject
方法。
// 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有兩種用途:
- 在某些場合,希望類的不同版本對序列化相容,因此需要確保類的不同版本具有相同的serialVersionUID;在某些場合,不希望類的不同版本對序列化相容, 因此需要確保類的不同版本具有不同的serialVersionUID。
- 當你序列化了一個類例項後,希望更改一個欄位或新增一個欄位,不設定serialVersionUID,所做的任何更改都將導致無法反序化舊有例項,並在反序列化時丟擲一個異常。 如果你添加了serialVersionUID,在反序列舊有例項時,新新增或更改的欄位值將設為初始化值(物件為null,基本型別為相應的初始預設值),欄位被刪除將不設定。
注意事項
- 序列化時,只對物件的狀態進行儲存,而不管物件的方法;
- 當一個父類實現序列化,子類自動實現序列化,不需要顯式實現Serializable介面;
- 當一個物件的例項變數引用其他物件,序列化該物件時也把引用物件進行序列化;
- 並非所有的物件都可以序列化,,至於為什麼不可以,有很多原因了,比如:
- 安全方面的原因,比如一個物件擁有private,public等field,對於一個要傳輸的物件,比如寫到檔案,或者進行rmi傳輸等等,在序列化進行傳輸的過程中,這個物件的private等域是不受保護的。
- 資源分配方面的原因,比如socket,thread類,如果可以序列化,進行傳輸或者儲存,也無法對他們進行重新的資源分 配,而且,也是沒有必要這樣實現。