中午一個網友來信說自己和麵試官幹起來了,看完他的描述真是苦笑不得,這年頭是怎麼了,最近網際網路CS訊息滿天飛,怎麼連面試官都SB起來了呢?
大概是這樣的:這位網友面試時被問及了Serializable介面的底層實現原理,因為這是一個標識性的空介面,大部分同學在學習時都秉持著會用就行(說實話,Build哥在這之前也沒怎麼細研究過,都是拿來就用),幾乎不太去關注底層的東西,這位網友亦是如此,在這種情況下,自然回答的心虛,這下可被面試官抓住了把柄,一頓帶有人身攻擊的狂輸出,讓面試現場變成了撕B現場,具體可看聊天截圖😂😂😂
基於這位網友的面試經歷,Build哥又趕緊去重新學了一下Serializable關鍵字,以及它背後的實現,別到時候咱也被暴懟,下面咱們一起來重溫一下。
一、序列化與反序列化
首先,我們先來了解一下兩個概念 序列化
與 反序列化
。
- 序列化: 將Java物件轉換為一個位元組序列(包含物件的資料、物件的型別和物件中儲存的屬性等資訊)的過程,以便於在網路上傳輸或者儲存在檔案中。
- 反序列化: 是序列化的逆過程,將位元組序列轉為Java物件的過程。
1.1 序列化與反序列化的應用場景
- 物件在進行網路傳輸(比如遠端方法呼叫 RPC 的時候)之前需要先被序列化,接收到序列化的物件之後需要再進行反序列化;
- 將物件儲存到檔案(如系統中excle的上傳與下載)之前需要進行序列化,將物件從檔案中讀取出來需要進行反序列化;
- 將物件儲存到資料庫(如 Redis)之前需要用到序列化,將物件從快取資料庫中讀取出來需要反序列化;
- 將物件儲存到記憶體之前需要進行序列化,從記憶體中讀取出來之後需要進行反序列化。
序列化與發序列化的流轉過程可參考下圖:
有個問題,如果在我的物件中,有些變數並不想被序列化應該怎麼辦呢?
答:不想被序列化的變數我們可以使用transient
或static
關鍵字修飾;transient 關鍵字的作用是阻止例項中那些用此關鍵字修飾的的變數序列化;當物件被反序列化時,被 transient 修飾的變數值不會被持久化和恢復;而static關鍵字修飾的變數並不屬於物件本身,所以也同樣不會被序列化!具體原因,我們在後面會解釋,繼續往下看。
二、Java中的序列流
為了探討Java物件序列化與反序列化的過程,以及Serializable關鍵字在整個過程中的作用,我們先來提一個 序列流
的概念,剛好我們最近也在寫關於Java IO的相關部落格。
Java 的序列流(ObjectInputStream 和 ObjectOutputStream)是一種可以將 Java 物件序列化和反序列化的流。這個屬於基本的位元組輸入流與輸出流的演變,之前的博文中已經介紹了它們的用法,在這裡就不再展開了。
- ObjectOutputStream:將序列化後的位元組序列寫入到檔案、網路等輸出流中。
- ObjectInputStream:可以讀取 ObjectOutputStream 寫入的位元組流,並將其反序列化為相應的物件(包含物件的資料、物件的型別和物件中儲存的屬性等資訊)。
三、序列化實戰
OK,有了上面兩個理論知識作為鋪墊,我們接下來就可以進行序列化的實戰了,首先,我們要先建立一個包含簡單屬性的類,這裡我們建立了一個Person類,裡面有name和age兩個屬性欄位。然後,我們透過ObjectOutputStream流將物件寫出到檔案(序列化),然後再透過ObjectInputStream讀取檔案中的資料,輸出為一個person物件(反序列化)。
話不多說,直接上程式碼:
public class Test {
public static void main(String[] args) throws IOException {
//初始化物件資訊
Person person = new Person();
person.setName("JavaBuild");
person.setAge(30);
System.out.println(person.getName()+" "+person.getAge());
//序列化過程
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:\\person.txt"));) {
objectOutputStream.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}
//反序列化過程
try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("E:\\person.txt"));) {
Person p = (Person) objectInputStream.readObject();
System.out.println(p.getName() + " " + p.getAge());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
然後我們執行一下,結果,哦吼!報錯了,提示了NotSerializableException,原因是我們在建立Person類時,並沒有實現Serializable介面。
很多初學的同學會很奇怪,跟進這個Serializable介面中發現裡面空空如也,為啥我們不實現它就無法進行序列化呢?
跟著上面報錯中的堆疊資訊,我們進入ObjectOutputStream的writeObject0方法中一探究竟!其中有部分原始碼如下:
// 判斷物件是否為字串型別,如果是,則呼叫 writeString 方法進行序列化
if (obj instanceof String) {
writeString((String) obj, unshared);
}
// 判斷物件是否為陣列型別,如果是,則呼叫 writeArray 方法進行序列化
else if (cl.isArray()) {
writeArray(obj, desc, unshared);
}
// 判斷物件是否為列舉型別,如果是,則呼叫 writeEnum 方法進行序列化
else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
}
// 判斷物件是否為可序列化型別,如果是,則呼叫 writeOrdinaryObject 方法進行序列化
else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
}
// 如果物件不能被序列化,則丟擲 NotSerializableException 異常
else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
從這段原始碼中我們可以發現,在序列化的時候,writeObject0方法內部會對物件進行型別判斷,包括字串、陣列、列舉或Serializable,這些條件都不滿足的話,就會丟擲NotSerializableException異常,因此,即便Serializable介面什麼都沒有,但需要是初始化的類實現了它的話,就滿足了obj instanceof Serializable,可以進行序列話操作!
我們將上面的測試程式碼中Person類實現Serializable介面後,再看結果:
序列化與反序列化都成功了,並獲得了預期的列印結果。
那麼它們的具體實現流程是怎麼樣的呢?
- 序列化: 以 ObjectOutputStream 為例吧,跟如它的原始碼時發現,它在序列化的時候會依次呼叫 writeObject()→writeObject0()→writeOrdinaryObject()→writeSerialData()→invokeWriteObject()→defaultWriteFields()。
- 反序列化: 以 ObjectInputStream 為例,它在反序列化的時候會依次呼叫 readObject()→readObject0()→readOrdinaryObject()→readSerialData()→defaultReadFields()。
四、總結
由此可見,Serializable 介面之所以定義為空,是因為它只起到了一個標識的作用,告訴程式實現了它的物件是可以被序列化的,但真正序列化和反序列化的操作並不需要它來完成,就像這裡的序列流才是主要實現序列化的驅動器!