Java程式設計師必備:序列化全方位解析

2021-03-02 編程如畫

前言

相信大家日常開發中,經常看到Java對象「implements Serializable」。那麼,它到底有什麼用呢?本文從以下幾個角度來解析序列這一塊知識點~

什麼是Java序列化?

為什麼需要序列化?

序列化用途

Java序列化常用API

序列化的使用

序列化底層

日常開發序列化的注意點

序列化常見面試題

一、什麼是Java序列化?

序列化:把Java對象轉換為字節序列的過程

反序列:把字節序列恢復為Java對象的過程 

二、為什麼需要序列化?

Java對象是運行在JVM的堆內存中的,如果JVM停止後,它的生命也就戛然而止。

如果想在JVM停止後,把這些對象保存到磁碟或者通過網絡傳輸到另一遠程機器,怎麼辦呢?磁碟這些硬體可不認識Java對象,它們只認識二進位這些機器語言,所以我們就要把這些對象轉化為字節數組,這個過程就是序列化啦~

打個比喻,作為大城市漂泊的碼農,搬家是常態。當我們搬書桌時,桌子太大了就通不過比較小的門,因此我們需要把它拆開再搬過去,這個拆桌子的過程就是序列化。而我們把書桌復原回來(安裝)的過程就是反序列化啦。

三、序列化用途

序列化使得對象可以脫離程序運行而獨立存在,它主要有兩種用途:

比如 Web伺服器中的Session對象,當有 10+萬用戶並發訪問的,就有可能出現10萬個Session對象,內存可能消化不良,於是Web容器就會把一些seesion先序列化到硬碟中,等要用了,再把保存在硬碟中的對象還原到內存中。

我們在使用Dubbo遠程調用服務框架時,需要把傳輸的Java對象實現Serializable接口,即讓Java對象序列化,因為這樣才能讓對象在網絡上傳輸。

四、Java序列化常用API

java.io.ObjectOutputStream

java.io.ObjectInputStream

java.io.Serializable

java.io.Externalizable

Serializable 接口

Serializable接口是一個標記接口,沒有方法或欄位。一旦實現了此接口,就標誌該類的對象就是可序列化的。

public interface Serializable {

}

Externalizable 接口

Externalizable繼承了Serializable接口,還定義了兩個抽象方法:writeExternal()和readExternal(),如果開發人員使用Externalizable來實現序列化和反序列化,需要重寫writeExternal()和readExternal()方法

public interface Externalizable extends java.io.Serializable {

void writeExternal(ObjectOutput out) throws IOException;

void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

}

java.io.ObjectOutputStream類

表示對象輸出流,它的writeObject(Object obj)方法可以對指定obj對象參數進行序列化,再把得到的字節序列寫到一個目標輸出流中。

java.io.ObjectInputStream

表示對象輸入流,它的readObject()方法,從輸入流中讀取到字節序列,反序列化成為一個對象,最後將其返回。

五、序列化的使用

序列化如何使用?來看一下,序列化的使用的幾個關鍵點吧:

聲明一個Student類,實現Serializable

public class Student implements Serializable {

private Integer age;

private String name;

public Integer getAge() {

return age;

}

public void setAge(Integer age) {

this.age = age;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

}

使用ObjectOutputStream類的writeObject方法,對Student對象實現序列化

把Student對象設置值後,寫入一個文件,即序列化,哈哈~

ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("D:\\text.out"));

Student student = new Student();

student.setAge(25);

student.setName("jayWei");

objectOutputStream.writeObject(student);

objectOutputStream.flush();

objectOutputStream.close();

看看序列化的可愛模樣吧,test.out文件內容如下(使用UltraEdit打開):

使用ObjectInputStream類的readObject方法,實現反序列化,重新生成student對象

再把test.out文件讀取出來,反序列化為Student對象

ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out"));

Student student = (Student) objectInputStream.readObject();

System.out.println("name="+student.getName());

六、序列化底層Serializable底層

Serializable接口,只是一個空的接口,沒有方法或欄位,為什麼這麼神奇,實現了它就可以讓對象序列化了?

public interface Serializable {

}

為了驗證Serializable的作用,把以上demo的Student對象,去掉實現Serializable接口,看序列化過程怎樣吧~

序列化過程中拋出異常啦,堆棧信息如下:

Exception in thread "main" java.io.NotSerializableException: com.example.demo.Student

at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)

at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)

at com.example.demo.Test.main(Test.java:13)

順著堆棧信息看一下,原來有重大發現,如下~原來底層是這樣:ObjectOutputStream 在序列化的時候,會判斷被序列化的Object是哪一種類型,String?array?enum?還是 Serializable,如果都不是的話,拋出 NotSerializableException異常。所以呀,Serializable真的只是一個標誌,一個序列化標誌~

writeObject(Object)

序列化的方法就是writeObject,基於以上的demo,我們來分析一波它的核心方法調用鏈吧~(建議大家也去debug看一下這個方法,感興趣的話)

writeObject直接調用的就是writeObject0()方法,

public final void writeObject(Object obj) throws IOException {

.

writeObject0(obj, false);

.

}

writeObject0 主要實現是對象的不同類型,調用不同的方法寫入序列化數據,這裡面如果對象實現了Serializable接口,就調用writeOrdinaryObject()方法~

private void writeObject0(Object obj, boolean unshared)

throws IOException

{

.

//String類型

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

//Serializable實現序列化接口

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

}

}

.

writeOrdinaryObject()會先調用writeClassDesc(desc),寫入該類的生成信息,然後調用writeSerialData方法,寫入序列化數據

private void writeOrdinaryObject(Object obj,

ObjectStreamClass desc,

boolean unshared)

throws IOException

{

.

//調用ObjectStreamClass的寫入方法

writeClassDesc(desc, false);

// 判斷是否實現了Externalizable接口

if (desc.isExternalizable() && !desc.isProxy()) {

writeExternalData((Externalizable) obj);

} else {

//寫入序列化數據

writeSerialData(obj, desc);

}

}

writeSerialData()實現的就是寫入被序列化對象的欄位數據,它會調用defaultWriteFields方法

private void writeSerialData(Object obj, ObjectStreamClass desc)

throws IOException

{

for (int i = 0; i < slots.length; i++) {

if (slotDesc.hasWriteObjectMethod()) {

//如果被序列化的對象自定義實現了writeObject()方法,則執行這個代碼塊

slotDesc.invokeWriteObject(obj, this);

} else {

// 調用默認的方法寫入實例數據

defaultWriteFields(obj, slotDesc);

}

}

}

defaultWriteFields()方法,獲取類的基本數據類型數據,直接寫入底層字節容器;獲取類的obj類型數據,循環遞歸調用writeObject0()方法,寫入數據~

private void defaultWriteFields(Object obj, ObjectStreamClass desc)

throws IOException

{

// 獲取類的基本數據類型數據,保存到primVals字節數組

desc.getPrimFieldValues(obj, primVals);

//primVals的基本類型數據寫到底層字節容器

bout.write(primVals, 0, primDataSize, false);

// 獲取對應類的所有欄位對象

ObjectStreamField[] fields = desc.getFields(false);

Object[] objVals = new Object[desc.getNumObjFields()];

int numPrimFields = fields.length - objVals.length;

// 獲取類的obj類型數據,保存到objVals字節數組

desc.getObjFieldValues(obj, objVals);

//對所有Object類型的欄位,循環

for (int i = 0; i < objVals.length; i++) {

.

//遞歸調用writeObject0()方法,寫入對應的數據

writeObject0(objVals[i],

fields[numPrimFields + i].isUnshared());

.

}

}

七、日常開發序列化的一些注意點

static靜態變量和transient 修飾的欄位是不會被序列化的

serialVersionUID問題

如果某個序列化類的成員變量是對象類型,則該對象類型的類必須實現序列化

子類實現了序列化,父類沒有實現序列化,父類中的欄位會丟失~

static靜態變量和transient 修飾的欄位是不會被序列化的

static靜態變量和transient 修飾的欄位是不會被序列化的,我們來看例子分析一波~ Student類加了一個類變量gender和一個transient修飾的欄位specialty

public class Student implements Serializable {

private Integer age;

private String name;

public static String gender = "男";

transient String specialty = "計算機專業";

public String getSpecialty() {

return specialty;

}

public void setSpecialty(String specialty) {

this.specialty = specialty;

}

@Override

public String toString() {

return "Student{" +"age=" + age + ", name='" + name + '\'' + ", gender='" + gender + '\'' + ", specialty='" + specialty + '\'' +

'}';

}

.

列印學生對象,序列化到文件,接著修改靜態變量的值,再反序列化,輸出反序列化後的對象~運行結果:

序列化前Student{age=25, name='jayWei', gender='男', specialty='計算機專業'}

序列化後Student{age=25, name='jayWei', gender='女', specialty='null'}

對比結果可以發現:

1)序列化前的靜態變量性別明明是『男』,序列化後再在程序中修改,反序列化後卻變成『女』了,what?顯然這個靜態屬性並沒有進行序列化。其實,靜態(static)成員變量是屬於類級別的,而序列化是針對對象的~所以不能序列化哦

2)經過序列化和反序列化過程後,specialty欄位變量值由'計算機專業'變為空了,為什麼呢?其實是因為transient關鍵字,它可以阻止修飾的欄位被序列化到文件中,在被反序列化後,transient 欄位的值被設為初始值,比如int型的值會被設置為 0,對象型初始值會被設置為null。

serialVersionUID問題

serialVersionUID 表面意思就是序列化版本號ID,其實每一個實現Serializable接口的類,都有一個表示序列化版本標識符的靜態變量,或者默認等於1L,或者等於對象的哈希碼。

private static final long serialVersionUID = -6384871967268653799L;

serialVersionUID有什麼用?

JAVA序列化的機制是通過判斷類的serialVersionUID來驗證版本是否一致的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID和本地相應實體類的serialVersionUID進行比較,如果相同,反序列化成功,如果不相同,就拋出InvalidClassException異常。

接下來,我們來驗證一下吧,修改一下Student類,再反序列化操作

Exception in thread "main" java.io.InvalidClassException: com.example.demo.Student;

local class incompatible: stream classdesc serialVersionUID = 3096644667492403394,

local class serialVersionUID = 4429793331949928814

at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687)

at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1876)

at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1745)

at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2033)

at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1567)

at java.io.ObjectInputStream.readObject(ObjectInputStream.java:427)

at com.example.demo.Test.main(Test.java:20)

從日誌堆棧異常信息可以看到,文件流中的class和當前類路徑中的class不同了,它們的serialVersionUID不相同,所以反序列化拋出InvalidClassException異常。那麼,如果確實需要修改Student類,又想反序列化成功,怎麼辦呢?可以手動指定serialVersionUID的值,一般可以設置為1L,或者讓我們的編輯器IDE生成

private static final long serialVersionUID = -6564022808907262054L;

實際上,阿里開發手冊,強制要求序列化類新增屬性時,不能修改serialVersionUID欄位~

如果某個序列化類的成員變量是對象類型,則該對象類型的類必須實現序列化

給Student類添加一個Teacher類型的成員變量,其中Teacher是沒有實現序列化接口的

public class Student implements Serializable {

private Integer age;

private String name;

private Teacher teacher;

...

}

//Teacher 沒有實現

public class Teacher {

.

}

序列化運行,就報NotSerializableException異常啦

Exception in thread "main" java.io.NotSerializableException: com.example.demo.Teacher

at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)

at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)

at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)

at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)

at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)

at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)

at com.example.demo.Test.main(Test.java:16)

其實這個可以在上小節的底層源碼分析找到答案,一個對象序列化過程,會循環調用它的Object類型欄位,遞歸調用序列化的,也就是說,序列化Student類的時候,會對Teacher類進行序列化,但是對Teacher沒有實現序列化接口,因此拋出NotSerializableException異常。所以如果某個序列化類的成員變量是對象類型,則該對象類型的類必須實現序列化

子類實現了Serializable,父類沒有實現Serializable接口的話,父類不會被序列化。

子類Student實現了Serializable接口,父類User沒有實現Serializable接口

//父類實現了Serializable接口

public class Student extends User implements Serializable {

private Integer age;

private String name;

}

//父類沒有實現Serializable接口

public class User {

String userId;

}

Student student = new Student();

student.setAge(25);

student.setName("jayWei");

student.setUserId("1");

ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\text.out"));

objectOutputStream.writeObject(student);

objectOutputStream.flush();

objectOutputStream.close();

//反序列化結果

ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out"));

Student student1 = (Student) objectInputStream.readObject();

System.out.println(student1.getUserId());

//output

/**

* null

*/

從反序列化結果,可以發現,父類屬性值丟失了。因此子類實現了Serializable接口,父類沒有實現Serializable接口的話,父類不會被序列化。

八、序列化常見面試題

序列化的底層是怎麼實現的?

序列化時,如何讓某些成員不要序列化?

在 Java 中,Serializable 和 Externalizable 有什麼區別

serialVersionUID有什麼用?

是否可以自定義序列化過程, 或者是否可以覆蓋 Java 中的默認序列化過程?

在 Java 序列化期間,哪些變量未序列化?

1.序列化的底層是怎麼實現的?

本文第六小節可以回答這個問題,如回答Serializable關鍵字作用,序列化標誌啦,源碼中,它的作用啦~還有,可以回答writeObject幾個核心方法,如直接寫入基本類型,獲取obj類型數據,循環遞歸寫入,哈哈~

2.序列化時,如何讓某些成員不要序列化?

可以用transient關鍵字修飾,它可以阻止修飾的欄位被序列化到文件中,在被反序列化後,transient 欄位的值被設為初始值,比如int型的值會被設置為 0,對象型初始值會被設置為null。

3.在Java中,Serializable 和 Externalizable 有什麼區別

Externalizable繼承了Serializable,給我們提供 writeExternal() 和 readExternal() 方法, 讓我們可以控制 Java的序列化機制, 不依賴於Java的默認序列化。正確實現 Externalizable 接口可以顯著提高應用程式的性能。

4.serialVersionUID有什麼用?

可以看回本文第七小節哈,JAVA序列化的機制是通過判斷類的serialVersionUID來驗證版本是否一致的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID和本地相應實體類的serialVersionUID進行比較,如果相同,反序列化成功,如果不相同,就拋出InvalidClassException異常。

5.是否可以自定義序列化過程, 或者是否可以覆蓋 Java 中的默認序列化過程?

可以的。我們都知道,對於序列化一個對象需調用 ObjectOutputStream.writeObject(saveThisObject), 並用 ObjectInputStream.readObject() 讀取對象, 但 Java 虛擬機為你提供的還有一件事, 是定義這兩個方法。如果在類中定義這兩種方法, 則 JVM 將調用這兩種方法, 而不是應用默認序列化機制。同時,可以聲明這些方法為私有方法,以避免被繼承、重寫或重載。

6.在Java序列化期間,哪些變量未序列化?

static靜態變量和transient 修飾的欄位是不會被序列化的。靜態(static)成員變量是屬於類級別的,而序列化是針對對象的。transient關鍵字修飾欄位,可以阻止該欄位被序列化到文件中。

參考與感謝

相關焦點

  • 全方位解析Java序列化
    本文從以下幾個角度來解析序列這一塊知識點~什麼是 Java 序列化?為什麼需要序列化?序列化用途Java 序列化常用 API序列化的使用序列化底層日常開發序列化的注意點序列化常見面試題一、什麼是Java序列化?
  • java反序列化——XMLDecoder反序列化漏洞
    前言最近學習java反序列化學到了weblogic部分,weblogic之前的兩個反序列化漏洞不涉及T3協議之類的,只是涉及到了XMLDecoder
  • JAVA反序列化—FastJson抗爭的一生
    組件api使用方法也很簡潔//序列化String text = JSON.toJSONString(obj);//反序列化VO vo = JSON.parse; //解析為JSONObject類型或者JSONArray類型VO vo = JSON.parseObject("{...}"); //
  • 詳解JAVA序列化
    這個時候我們就需要通過固定的協議,傳輸固定的數據格式,而這個數據傳輸的協議稱之為序列化,而定義了傳輸數據行為的框架組件也稱之為序列化組件(框架)序列化有什麼意義首先我們先看看,java中的序列化,在java語言中實例對象想要序列化傳輸,需要實現Serializable 接口,只有當前接口修飾定義的類對象才可以按照指定的方式傳輸對象。
  • Java程式設計師面試必備:Volatile全方位解析
    前言volatile是Java程式設計師必備的基礎,也是面試官非常喜歡問的一個話題,本文跟大家一起開啟vlatile學習之旅,如果有不正確的地方,也麻煩大家指出哈,一起相互學習~1.volatile的用法 2.vlatile變量的作用 3.現代計算機的內存模型(計算機模型
  • 【Java拾遺】不可不知的 Java 序列化
    首先我們要把想序列化的類實現 Java 自帶的 java.io.Serializable 接口/* * *  * * *  *  * blog.coder4j.cn *  *  * Copyright (C) 2016-2020 All Rights Reserved.
  • Java安全之反序列化漏洞分析
    序列化與反序列化對於Java程式設計師來說,應該不算陌生了,序列化與反序列化簡單來說就是Java對象與數據之間的相互轉化。那麼對於完全面向對象的Java語言來說為什麼要有序列化機制?實質上,序列化機制並不只局限於Java語言,序列化的本質是內存對象到數據流的一種轉換,我們知道內存中的東西不具備持久性,但有些場景卻需要將對象持久化保存或傳輸。
  • 阿里內部學習指南《Effective Java中文 第3版》程式設計師進階必備
    經典Jolt獲獎作品《Effective Java》的第3版這本書,對上一版內容進行了徹底的更新,介紹了如何充分利用從泛型到枚舉、從註解到自動裝箱的各種特性,幫助讀者更加有效地使用Java程式語言及其基本類庫:java.lang. java.util和java.io,以及子包,如java.util
  • Java 序列化的這三個坑千萬要小心
    ,其支持的序列化協議一共有四種:dubbo序列化:阿里尚未開發成熟的高效java序列化實現,阿里不建議在生產環境使用它hessian2序列化:hessian是一種跨語言的高效二進位序列化方式。java序列化:主要是採用JDK自帶的Java序列化實現,性能很不理想。從那個帖子看當時HSF服務提供集群設置的序列化方式是java序列化,而不是像現在一樣默認hessian2,如果在RPC中使用了Java序列化,那下面的這三個坑一定注意不要踩
  • 關於 Java 對象序列化您不知道的 5 件事
    實際上,序列化的思想是 「凍結」 對象狀態,傳輸對象狀態(寫到磁碟、通過網絡傳輸等等),然後 「解凍」 狀態,重新獲得可用的 Java 對象。所有這些事情的發生有點像是魔術,這要歸功於 ObjectInputStream/ObjectOutputStream 類、完全保真的元數據以及程式設計師願意用Serializable 標識接口標記他們的類,從而 「參與」 這個過程。
  • Java程式設計師必備基礎:Java代碼是怎麼運行的?
    前言 作為一名Java程式設計師,我們需要知道Java代碼是怎麼運行的。最近複習了深入理解Java虛擬機,做了一下總結,希望對大家有幫助,如果有不正確的地方,歡迎提出,感激不盡。
  • 關於 Java 序列化你不知道的 5 件事
    事實上,大多數程式設計師對於 Java 平臺都是淺嘗則止,只學習了足以完成手頭上任務的知識而已。在本系列 中,Ted Neward 深入挖掘 Java 平臺的核心功能,揭示一些鮮為人知的事實,幫助您解決最棘手的編程挑戰。
  • Java 8u20反序列化漏洞分析
    二、Java序列化過程及數據分析在8u20的POC中需要直接操作序列化文件結構,需要對Java序列化數據寫入過程、數據結構和數據格式有所了解。public B() { }}import java.io.
  • 明晰 | Java序列化與反序列化
    實戰含義序列化:對象寫入IO流中,實現對象變成文件。反序列化:把文件中的對象,進行恢復,恢復到內存中,實現反序列化。意義:序列化的最大的意義在於實現對象可以跨主機進行傳輸,這些對象可以實現保存在磁碟中,並且脫離程序而獨立存在。
  • 深入剖析 Java 反序列化漏洞
    但是,高端的玩家往往不會僅限於此,熟悉接口開發的同學一定知道,能將數據對象很輕鬆的實現多平臺之間的通信、對象持久化存儲,序列化和反序列化是一種非常有效的手段,例如如下應用場景,對象必須 100% 實現序列化。
  • java反序列化之Commons Collections分析(一)
    前言在學習java反序列化的過程中,Commons Collections幾乎是反序列化學習中無法繞過的一關。也是各大ctf和awd的常見考點,作為java代碼審計的重要一環,我們今天就來解析一下Commons Collections利用鏈。
  • 一文看懂Java序列化之serialVersionUID
    serialVersionUID適用於Java的序列化機制。簡單來說,Java的序列化機制是通過判斷類的serialVersionUID來驗證版本一致性的。當實現java.io.Serializable接口的類沒有顯式地定義一個serialVersionUID變量時候,Java序列化機制會根據編譯的Class自動生成一個serialVersionUID作序列化版本比較用,這種情況下,如果Class文件(類名,方法明等)沒有發生變化(增加空格,換行,增加注釋等等),就算再編譯多次,serialVersionUID也不會變化的。
  • 教你徹底學會Java序列化和反序列化
    Java序列化是指把Java對象轉換為字節序列的過程,Java反序列化是指把字節序列恢復為Java對象的過程。反序列化:客戶端重文件,或者網絡中獲取到文件以後,在內存中重構對象。序列化:對象序列化的最重要的作用是傳遞和保存對象的時候,保證對象的完整性和可傳遞性。方便字節可以在網絡上傳輸以及保存在本地文件。
  • Java程式設計師必備學習資源大全(五)
    序列化用來高效處理序列化的函數庫。FlatBuffers:高效利用內存的序列化函數庫,無需解包和解析即可高效訪問序列化數據。Kryo:快速、高效的對象圖形序列化框架。FST:提供兼容JDK的高性能對象圖形序列化。MessagePack:一種高效的二進位序列化格式。
  • Java序列化的這三個坑你可千萬要小心,別又掉進去了~
    >dubbo序列化:阿里尚未開發成熟的高效java序列化實現,阿里不建議在生產環境使用它hessian2序列化:hessian是一種跨語言的高效二進位序列化方式。java序列化:主要是採用JDK自帶的Java序列化實現,性能很不理想。