前言
在開發過程中經常會對實體進行序列化,但其實我們只是在“只知其然,不知其所以然”的狀態,很多時候會有這些問題:
- 什麼是序列化和反序列化?爲什麼要序列化?
- 怎麼實現序列化?
- 序列化的原理是什麼呢?
- transient關鍵字
- 序列化時應注意什麼?
如果你也有這些疑問,不妨看看本文?
(若文章有不正之處,或難以理解的地方,請多多諒解,歡迎指正)
什麼是序列化和反序列化?
Java序列化是指把Java對象轉換爲字節序列的過程;
Java反序列化是指把字節序列恢復爲Java對象的過程;
爲什麼要序列化?
其實我們的對象不只是存儲在內存中,它還需要在傳輸網絡中進行傳輸,並且保存起來之後下次再加載出來,這時候就需要序列化技術。
- 一般Java對象的生命週期比Java虛擬機端,而實際開發中如果需要JVM停止後能夠繼續持有對象,則需要用到序列化技術將對象持久化到磁盤或數據庫。
- 在多個項目進行RPC調用時,需要在網絡上傳輸JavaBean對象,而網絡上只允許二進制形式的數據進行傳輸,這時則需要用到序列化技術。
Java的序列化技術就是把對象轉換成一串由二進制字節組成的數組,然後將這二進制數據保存在磁盤或傳輸網絡。而後需要用到這對象時,磁盤或者網絡接收者可以通過反序列化得到此對象,達到對象持久化的目的。
怎麼實現序列化?
序列化的過程一般會是這樣的:
- 將對象實例相關的類元數據輸出
- 遞歸地輸出類的超類描述,直到沒有超類
- 類元數據輸出之後,開始從最頂層的超類輸出對象實例的實際數據值
- 從上至下遞歸輸出實例的數據
所以,如果父類已經序列化了,子類繼承之後也可以進行序列化。
實現第一步,則需要的先將對象實例相關的類標記爲需要序列化。
實現序列化的要求:目標對象實現Serializable接口
我們先創建一個NY類,實現Serializable接口,並生成一個版本號:
public class NY implements Serializable {
private static final long serialVersionUID = 8891488565683643643L; //使用idea生成
private String name;
private String blogName;
//省略getter和setter...
@Override
public String toString() {
return "NY{" +
"name='" + name + '\'' +
", blogName='" + blogName + '\'' +
'}';
}
}
在這裏,Serializable接口的作用只是標識這個類是需要進行序列化的,而且Serializable接口中並沒有提供任何方法。而且serialVersionUID序列化版本號的作用是用來區分我們所編寫的類的版本,用於反序列化時確定版本。
JDK類庫中序列化和反序列化API
-
java.io.ObjectInputStream:對象輸入流
該類中的readObject()方法從輸入流中讀取字節序列,然後將字節序列反序列化爲一個對象並返回。
-
java.io.ObjectOutputStream:對象輸出流
該類的writeObject()方法將傳入的obj對象進行序列化,把得到的字節序列寫入到目標輸出流中進行輸出。
結合上面的NY類,我們來看看使用JDK類庫中的API怎麼實現序列化和反序列化:
public class SerializeNY {
public static void main(String[] args) throws IOException, ClassNotFoundException {
serializeNY();
NY ny = deserializeNY();
System.out.println(ny.toString());
}
private static void serializeNY() throws IOException {
NY ny = new NY();
ny.setName("NY");
ny.setBlogName("NYfor2020");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:\\serialable.txt")));
oos.writeObject(ny);
System.out.println("NY 對象序列化成功!");
oos.close();
}
private static NY deserializeNY() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:\\serialable.txt")));
NY ny = (NY) ois.readObject();
System.out.println("NY 對象反序列化成功");
return ny;
}
}
運行結果爲:
NY 對象序列化成功!
NY 對象反序列化成功
NY{name='NY', blogName='NYfor2020'}
可以看到,這整個過程簡單來說就是把對象存在磁盤,然後再從磁盤讀出來。
但是我們平時看到序列化的實體中的serialVersionUID,爲什麼有的是1L,有的是一長串數字?
上面我們的提到serialVersionUID作用就是用來區分類的版本,所以無論是1L還是一長串數字,都是用來確認版本的。如果序列化的類版本改變,則在反序列化的時候就會報錯。
舉個栗子,剛剛我們已經在磁盤中生成了NY對象的序列化文件,如果我們對NY類的serialVersionUID稍作改動,改成:
private static final long serialVersionUID = 8891488565683643643L; //將末尾的2改成3
再執行一次反序列化方法,運行結果如下:
Exception in thread "main" java.io.InvalidClassException: NY; local class incompatible: stream classdesc serialVersionUID = 8891488565683643642, local class serialVersionUID = 8891488565683643643
......
至於怎麼讓idea生成serialVersionUID,則需要在idea設置中改個配置即可:
之後再使用"Alt+Enter"鍵即可調出下圖選項:
序列化的原理是什麼呢?
既然知道了序列化是怎麼使用的,那麼序列化的原理是怎麼樣的呢?
我們用上面的例子來作爲探尋序列化原理的入口:
private static void serializeNY() throws IOException {
NY ny = new NY();
ny.setName("NY");
ny.setBlogName("NYfor2020");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:\\serialable.txt")));
oos.writeObject(ny);
System.out.println("NY 對象序列化成功!");
oos.close();
}
- 進入ObjectOutputStream的構造函數:
public ObjectOutputStream(OutputStream out) throws IOException {
//檢查是否爲ObjectOutputStream的實例
verifySubclass();
//bout是底層的數據字節容器
bout = new BlockDataOutputStream(out);
handles = new HandleTable(10, (float) 3.00);
subs = new ReplaceTable(10, (float) 3.00);
enableOverride = false;
//寫入序列化文件頭
writeStreamHeader();
//設置文件緩存刷新配置
bout.setBlockDataMode(true);
if (extendedDebugInfo) {
debugInfoStack = new DebugTraceInfoStack();
} else {
debugInfoStack = null;
}
}
- 我們進入**writeStreamHeader()**方法:
protected void writeStreamHeader() throws IOException {
bout.writeShort(STREAM_MAGIC);
bout.writeShort(STREAM_VERSION);
}
- 這個方法是將序列化文件的魔數和版本寫入序列化文件頭:
/**
* Magic number that is written to the stream header.
*/
final static short STREAM_MAGIC = (short)0xaced;
/**
* Version number that is written to the stream header.
*/
final static short STREAM_VERSION = 5;
- 在**writeObject()**方法進行具體的序列化寫入操作:
public final void writeObject(Object obj) throws IOException {
//表示使用writeObjectOverride()方法進行序列化寫入,一般爲不執行
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
//調用writeObject0()方法進行具體的序列化操作
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
- 進入**writeObject0()**方法:
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
//此處對緩存刷新進行默認配置
boolean oldMode = bout.setBlockDataMode(false);
//遞歸深度
depth++;
try {
// handle previously written and non-replaceable objects
int h;
if ((obj = subs.lookup(obj)) == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
// check for replacement object
Object orig = obj;
//需要序列的對象的Class對象
Class<?> cl = obj.getClass();
ObjectStreamClass desc;
for (;;) {
// REMIND: skip this check for strings/arrays?
Class<?> repCl;
//創建描述c1的ObjectStreamClass對象
desc = ObjectStreamClass.lookup(cl, true);
//判斷是否有可替換的寫方法,一般是沒有的
if (!desc.hasWriteReplaceMethod() ||
(obj = desc.invokeWriteReplace(obj)) == null ||
(repCl = obj.getClass()) == cl)
{
break;
}
cl = repCl;
}
//判斷此對象是否可以被替換
if (enableReplace) {
Object rep = replaceObject(obj);
if (rep != obj && rep != null) {
cl = rep.getClass();
desc = ObjectStreamClass.lookup(cl, true);
}
obj = rep;
}
// if object replaced, run through original checks a second time
//如果這個obj對象被替換了
if (obj != orig) {
subs.assign(orig, obj);
if (obj == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
}
// remaining cases
//根據實際要寫入的類型,進行不同的寫入操作
//其中,String、Array、Enum類型是直接寫入操作的
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) {
//實現序列化接口的對象都會執行下面的方法
//其實在這裏就可以看出,Serializable只是一個標記接口,其本身並沒有什麼意義
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
} finally {
//此層遞歸結束
depth--;
bout.setBlockDataMode(oldMode);
}
}
這一段代碼中創建了ObjectStreamClass對象,並根據不同的對象類型來執行不同的寫入操作。而在此例子中,對象對應的類實現了Serializable接口,所以下一步會執行writeOrdinaryObject()方法。
- **writeOrdinaryObject()**是當對象對應的類實現了Serializable接口的時纔會被調用:
private void writeOrdinaryObject(Object obj,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
if (extendedDebugInfo) {
debugInfoStack.push(
(depth == 1 ? "root " : "") + "object (class \"" +
obj.getClass().getName() + "\", " + obj.toString() + ")");
}
try {
desc.checkSerialize();
//寫入Object的標記位符號,表示這是一個新的Object對象
bout.writeByte(TC_OBJECT);
//將對類的描述寫入
writeClassDesc(desc, false);
handles.assign(unshared ? null : obj);
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);
} else {
//寫入序列化對象具體的實例數據
writeSerialData(obj, desc);
}
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
- 接下來是將類的描述寫入類元數據中的writeClassDesc():
private void writeClassDesc(ObjectStreamClass desc, boolean unshared)
throws IOException
{
int handle;
if (desc == null) {
//如果desc爲null,則寫入null
writeNull();
} else if (!unshared && (handle = handles.lookup(desc)) != -1) {
writeHandle(handle);
} else if (desc.isProxy()) {
writeProxyDesc(desc, unshared);
} else {
writeNonProxyDesc(desc, unshared);
}
}
- 在desc爲null時,會執行**writeNull()**方法:
private void writeNull() throws IOException {
bout.writeByte(TC_NULL);
}
/**
* Null object reference.
*/
final static byte TC_NULL = (byte)0x70;
可以看到,在writeNull()中,會將表示NULL的標識寫入序列中。
- 那麼如果desc不爲null時,一般執行**writeNonProxyDesc()**方法:
private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
throws IOException
{
//類元信息的標記位
//表示接下來的數據爲Class描述符
bout.writeByte(TC_CLASSDESC);
handles.assign(unshared ? null : desc);
if (protocol == PROTOCOL_VERSION_1) {
// do not invoke class descriptor write hook with old protocol
desc.writeNonProxy(this);
} else {
//一般會執行此方法,將類描述寫入
writeClassDescriptor(desc);
}
Class<?> cl = desc.forClass();
bout.setBlockDataMode(true);
if (cl != null && isCustomSubclass()) {
ReflectUtil.checkPackageAccess(cl);
}
//根據cl的類型進行處理
annotateClass(cl);
bout.setBlockDataMode(false);
//表示對一個object的描述塊的結束
bout.writeByte(TC_ENDBLOCKDATA);
‘
//此處會將對象相應的類的父類寫入
writeClassDesc(desc.getSuperDesc(), false);
}
- 在上一個方法執行過程中,會執行**writeClassDescriptor()**方法將類的描述寫入類元數據中:
protected void writeClassDescriptor(ObjectStreamClass desc)
throws IOException{
desc.writeNonProxy(this);
}
- 在這裏我們可以看到,寫入類元信息的方法調用了**writeNonProxy()**方法:
void writeNonProxy(ObjectOutputStream out) throws IOException {
//寫入類名
out.writeUTF(name);
//寫入serialVersionUID,看!這裏顯示了序列號的重要性
out.writeLong(getSerialVersionUID());
//類的標記
byte flags = 0;
if (externalizable) {
flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;
int protocol = out.getProtocolVersion();
if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) {
flags |= ObjectStreamConstants.SC_BLOCK_DATA;
}
} else if (serializable) {
//一般程序會執行到這裏,標識類執行序列化
// final static byte SC_SERIALIZABLE = 0x02;
flags |= ObjectStreamConstants.SC_SERIALIZABLE;
}
if (hasWriteObjectData) {
// 自定義writeObject方法
// final static byte SC_WRITE_METHOD = 0x01;
flags |= ObjectStreamConstants.SC_WRITE_METHOD;
}
if (isEnum) {
//枚舉標記
//final static byte SC_ENUM = 0x10;
flags |= ObjectStreamConstants.SC_ENUM;
}
//將標記寫入類元信息中
out.writeByte(flags);
//寫入對象的字段數量
out.writeShort(fields.length);
for (int i = 0; i < fields.length; i++) {
ObjectStreamField f = fields[i];
//寫入字段類型對象的Code,類似標記
out.writeByte(f.getTypeCode());
//寫入字段的名字
out.writeUTF(f.getName());
if (!f.isPrimitive()) {
//如果是對象或接口,則會寫入表示對象的字符串
out.writeTypeString(f.getTypeString());
}
}
}
這次方法中我們可以看到:
- 調用writeUTF()方法將對象所屬類的名字寫入。
- 調用writeLong()方法將類的序列號serialVersionUID寫入。
- 判斷被序列化對象所屬類的流類型flag,寫入底層字節容器中(佔兩個字節)。
- 寫入對象中的所有字段,以及對應的屬性
所以直到這個方法的執行,一個對象及其對應類的所有屬性和屬性值才被序列化。當上述流程完成之後,回到**writeOrdinaryObject()**方法中,繼續往下運行:
private void writeOrdinaryObject(Object obj,ObjectStreamClass desc,boolean unshared)
throws IOException{
...
writeClassDesc(desc, false);
//------------------------
//程序會在這裏繼續往下運行,已將對象的相關序列化數據寫入流中了
//------------------------
handles.assign(unshared ? null : obj);
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);
} else {
//將序列化對象的實例化數據寫入
writeSerialData(obj, desc);
}
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
- 調用**writeSerialData()**方法將實例化數據寫入:
private void writeSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
//獲取序列化對象的數據佈局ClassDataSlot
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
//如果序列化對象實現了自己的writeObject()方法,則進入if代碼塊,否則進入else代碼塊,執行默認的寫入方法
if (slotDesc.hasWriteObjectMethod()) {
PutFieldImpl oldPut = curPut;
curPut = null;
SerialCallbackContext oldContext = curContext;
if (extendedDebugInfo) {
debugInfoStack.push(
"custom writeObject data (class \"" +
slotDesc.getName() + "\")");
}
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bout.setBlockDataMode(true);
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
} finally {
curContext.setUsed();
curContext = oldContext;
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
curPut = oldPut;
} else {
//一般執行這個方法,默認的寫入實例數據
defaultWriteFields(obj, slotDesc);
}
}
}
- 當執行到**defaultWriteFields()**方法時,會將實例數據寫入:
private void defaultWriteFields(Object obj, ObjectStreamClass desc)
throws IOException
{
Class<?> cl = desc.forClass();
if (cl != null && obj != null && !cl.isInstance(obj)) {
throw new ClassCastException();
}
//檢查是否可以使用默認的序列化
desc.checkDefaultSerialize();
int primDataSize = desc.getPrimDataSize();
if (primVals == null || primVals.length < primDataSize) {
primVals = new byte[primDataSize];
}
//獲取對象中基本類型的實例數據,並將其放到primVals數組中
desc.getPrimFieldValues(obj, primVals);
//將對象基本類型的實例數據,寫入底層的字節緩衝流
bout.write(primVals, 0, primDataSize, false);
//獲取類對應的引用類型的字段對象
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
//對象的字段對象值
desc.getObjFieldValues(obj, objVals);
//將對應的對象類型字段保存到objVals數組中
for (int i = 0; i < objVals.length; i++) {
if (extendedDebugInfo) {
debugInfoStack.push(
"field (class \"" + desc.getName() + "\", name: \"" +
fields[numPrimFields + i].getName() + "\", type: \"" +
fields[numPrimFields + i].getType() + "\")");
}
try {
//對序列化對象中引用類型的字段,調用writeObject0()寫入對應的數據
writeObject0(objVals[i],
fields[numPrimFields + i].isUnshared());
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
}
在執行完上述方法之後,程序將會回到writeNonProxyDesc()方法中,並且在writeClassDesc()中會將對象對應的類的父類信息進行寫入:
private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
throws IOException
{
...
//一般會執行此方法,將類描述寫入
writeClassDescriptor(desc);
}
Class<?> cl = desc.forClass();
bout.setBlockDataMode(true);
if (cl != null && isCustomSubclass()) {
ReflectUtil.checkPackageAccess(cl);
}
//根據cl的類型進行處理
annotateClass(cl);
bout.setBlockDataMode(false);
//表示對一個object的描述塊的結束
bout.writeByte(TC_ENDBLOCKDATA);
‘
//此處會將對象相應的類的父類寫入
writeClassDesc(desc.getSuperDesc(), false);
}
至此,我們可以知道,整個序列化的過程其實就是一個遞歸寫入的過程。
將上面的過程進行簡化,可以總結爲這幅圖:
transient關鍵字
在有些時候,我們並不想將一些敏感信息序列化,如密碼等,這個時候就需要transient關鍵字來標註屬性爲非序列化屬性。
transient關鍵字的使用
將上面的NY類中的name屬性稍作修改:
private transient String name;
當我們再次運行SerializeNY類中的main()方法時,運行結果如下:
NY 對象序列化成功!
NY 對象反序列化成功
NY{name='null', blogName='NYfor2020'}
我們可以看到,name屬性爲null,說明反序列化時根本沒有從文件中獲取到信息。
transient關鍵字的特點
-
變量一旦被transient修飾,則不再是對象持久化的一部分了,而且變量內容在反序列化時也不能獲得。
-
transient關鍵字只能修飾變量,而不能修飾方法和類,而且本地變量是不能被transient修飾的,如果變量是類變量,則需要該類也實現Serializable接口。
-
一個靜態變量不管是否被transient修飾,都不會被序列化。
關於這一點,可能會有讀者感到疑惑。舉個栗子,如果用static修飾NY類中的name:
private static String name;
運行SerializeNY類中的main程序,可以看到運行結果:
NY 對象序列化成功! NY 對象反序列化成功 NY{name='NY', blogName='NYfor2020'}
嘶…這是翻車了嗎?並沒有,因爲這裏出現的name值是當前JVM中對應的static變量值,這個值是JVM中的而不是反序列化得出的。
不信?我們來改變一下SerializeNY類中的**serializeNY()**函數:
private static void serializeNY() throws IOException { NY ny = new NY(); ny.setName("NY"); ny.setBlogName("NYfor2020"); ny.setTest("12"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:\\serialable.txt"))); oos.writeObject(ny); System.out.println("NY 對象序列化成功!"); System.out.println(ny.toString()); oos.close(); ny.setName("hey, NY"); }
筆者在NY對象被序列化之後,改變了NY對象的name值。運行結果爲:
NY 對象序列化成功! NY{name='NY', blogName='NYfor2020'} NY 對象反序列化成功 NY{name='hey, NY', blogName='NYfor2020'}
transient修飾的變量真的就不能被序列化了嗎?
舉個栗子:
public class ExternalizableTest implements Externalizable {
private transient String content = "即使被transient修飾,我也會序列化";
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(content);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
content = (String)in.readObject();
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
ExternalizableTest et = new ExternalizableTest();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:\\externalizable.txt")));
oos.writeObject(et);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:\\externalizable.txt")));
et = (ExternalizableTest) ois.readObject();
System.out.println(et.content);
oos.close();
ois.close();
}
}
運行結果爲:
即使被transient修飾,我也會序列化
我們可以看到,content變量在被transient修飾的情況下,還是被序列化了。因爲在Java中,對象序列化可以通過實現兩種接口來實現:
- 如果實現的是Serializable接口,則所有信息(不包括被static、transient修飾的變量信息)的序列化將自動進行。
- 如果實現的是Externalizable接口,則不會進行自動序列化,需要開發者在writeExternal()方法中手工指定需要序列化的變量,與是否被transient修飾無關。
序列化注意事項
- 序列化對象必須實現序列化接口Serializable。
- 序列化對象中的屬性如果也有對象的話,其對象需要實現序列化接口。
- 類的對象序列化後,類的序列號不能輕易更改,否則反序列化會失敗。
- 類的對象序列化後,類的屬性增加或刪除不會影響序列化,只是值會丟失。
- 如果父類序列化,子類會繼承父類的序列化;如果父類沒序列化,子類序列化了,子類中的屬性能正常序列化,但父類的屬性會丟失,不能序列化。
- 用Java序列化的二進制字節數據只能由Java反序列化,如果要轉換成其他語言反序列化,則需要先轉換成Json/XML通用格式的數據。
- 如果某個字段不想序列化,在該字段前加上transient關鍵字即可。(咳咳,下一篇就是寫這個了,敬請關注~)
結語
第一次寫關於JDK實現原理的文章,還是覺得有點難度的,但是這對於源碼分析能力還是有點提升的。在這個過程中最好多打斷點,多調試。
如果本文對你的學習有幫助,請給一個贊吧,這會是我最大的動力~
參考資料:
Java 之 Serializable 序列化和反序列化的概念,作用的通俗易懂的解釋
本文已授權發佈在微信公衆號:Java後端。