前言
序列化和反序列化看起來用的不多,但用起來就很關鍵,因為稍一不注意就會出現問題。序列化的應用場景在哪里?當然是數據存儲和傳輸。比如緩存,需要將對象復刻到硬盤存儲,即使斷電也可以重新反序列化恢復。下面簡單理解序列化的用法以及注意事項。
如何序列化
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類,如果可以序列化,進行傳輸或者保存,也無法對他們進行重新的資源分 配,而且,也是沒有必要這樣實現。