1. 程式人生 > >Java程式設計師必備:序列化全方位解析

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

前言

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

  • 什麼是Java序列化?
  • 為什麼需要序列化?
  • 序列化用途
  • Java序列化常用API
  • 序列化的使用
  • 序列化底層
  • 日常開發序列化的注意點
  • 序列化常見面試題

一、什麼是Java序列化?

  • 序列化:把Java物件轉換為位元組序列的過程
  • 反序列:把位元組序列恢復為Java物件的過程

二、為什麼需要序列化?

Java物件是執行在JVM的堆記憶體中的,如果JVM停止後,它的生命也就戛然而止。


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

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

三、序列化用途

序列化使得物件可以脫離程式執行而獨立存在,它主要有兩種用途:

  • 1) 序列化機制可以讓物件地儲存到硬碟上,減輕記憶體壓力的同時,也起了持久化的作用;

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

  • 2) 序列化機制讓Java物件在網路傳輸不再是天方夜譚。

我們在使用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()方法,從輸入流中讀取到位元組序列,反序列化成為一個物件,最後將其返回。

五、序列化的使用

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

  • 宣告一個實體類,實現Serializable介面
  • 使用ObjectOutputStream類的writeObject方法,實現序列化
  • 使用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()實現的就是寫入被序列化物件的欄位資料

  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物件的序列化和反序列化
  • 10個艱難的Java面試題與答案

個人公眾號

  • 覺得寫得好的小夥伴給個點贊+關注啦,謝謝~
  • 如果有寫得不正確的地方,麻煩指出,感激不盡。
  • 同時非常期待小夥伴們能夠關注我公眾號,後面慢慢推出更好的乾貨~嘻嘻