Java序列化和反序列化機制

来源:https://www.cnblogs.com/void-cmy/p/18074769
-Advertisement-
Play Games

Java的序列化和反序列化機制 問題導入: 在閱讀ArrayList源碼的時候,註意到,其內部的成員變數動態數組elementData被Java中的關鍵字transient修飾 transient關鍵字意味著Java在序列化時會跳過該欄位(不序列化該欄位) 而Java在預設情況下會序列化類(實現了J ...


Java的序列化和反序列化機制

問題導入:

在閱讀ArrayList源碼的時候,註意到,其內部的成員變數動態數組elementData被Java中的關鍵字transient修飾

transient關鍵字意味著Java在序列化時會跳過該欄位(不序列化該欄位)

而Java在預設情況下會序列化類(實現了Java.io.Serializable介面的類)的所有非瞬態(未被transient關鍵字修飾)和非靜態('未被static關鍵字修飾')欄位

為什麼ArrayList要給非常重要的動態數組成員變數elementData添加transient關鍵字?

事實上,ArrayListelementData添加transient關鍵字的原因是因為Java預設的序列化方法並不理想

  • 空間效率: 由於擴容機制,elementData數組的容量可能會大於實際存儲的元素數量,數組中可能存在未使用的空間,如果直接走Java預設的序列化,直接序列化整個數組,會將這部分未使用的空間也一起序列化,導致空間浪費
  • 控制序列化行為: 通過自定義writeObject()readObject()方法,ArrayList能夠更好地控制序列化和反序列化過程,僅序列化實際包含的元素,併在反序列化時重新創建合適的數組大小

那麼,Java的序列化機制,標識介面Java.io.Serializable和關鍵字transient等是如何運作的?

從兩個類說起

Java中實現序列化和反序列化的兩個核心類是ObjectInputStreamObjectOutputStream

  • ObjectOutputStream:將Java對象的原始數據類型以流的方式寫出到文件,實現對象的持久化存儲
  • ObjectInputStream:將文件中保存的對象,以流的方式取出來使用

一個簡單的示例

//1.創建一個類 實現序列化介面(標識該類可被序列化,如果不實現該介面,調用序列化方法會報java.io.NotSerializableException)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Serializable {

    private String name;

    private Integer age;

    //標記remark欄位 不會被序列化
    private transient String remark;

}
//2.序列化和反序列化演示
@Test
public void test(){

    //創建對象
    Person person = new Person();
    person.setName("void");
    person.setAge(26);
    person.setRemark("hello world");

    //指定 目標位置
    String target = "F:\\out\\s.txt";

    //序列化 演示
    try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(Files.newOutputStream(Paths.get(target)))) {

        objectOutputStream.writeObject(person);

    } catch (IOException e) {
        e.printStackTrace();
    }

    //反序列化 演示
    try (ObjectInputStream objectInputStream = new ObjectInputStream(Files.newInputStream(Paths.get(target)))) {

        Person person1 = (Person) objectInputStream.readObject();
        log.info("person1:{}", person1);
        //person1:Person(name=void, age=26, remark=null) 註意這裡的remark欄位,有transient關鍵字修飾和沒有是兩個結果
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
}

源碼解析

前文說到

  • Serializable起標識作用,標識該類可被序列化,如果不實現該介面,調用序列化方法會報java.io.NotSerializableException
  • transient關鍵字標記的欄位不會被序列化
    從源碼來驗證:

Serializable起標識作用原理
java.io.ObjectOutputStream#writeObject0()方法中的代碼片段
可以看到,如果這個類既不是字元串,數組,枚舉類,也沒有實現Serializable介面,就會報(NotSerializableException)錯

private void writeObject0(Object obj, boolean unshared)
        throws IOException
{
        ...
        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());
           }
        }
        ...
}
//...

transient關鍵字標記的欄位不會被序列化原理
java.io.ObjectStreamClass.getDefaultSerialFields中的代碼片段
這裡涉及一種關鍵的數學和電腦科學知識點,即通過位運算,一個整數能夠被精確無誤地分解為多個具有唯一確定性的二進位子串。換言之,對於任何整數,我們都可以利用位運算技術將其分割成多個獨一無二、確定無疑的二進位表示狀態

private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
        Field[] clFields = cl.getDeclaredFields();
        ArrayList<ObjectStreamField> list = new ArrayList<>();
        //註意點1: Modifier 是 Java中用來表示修飾符的一個類 一個整數可以通過位運算聚合多種狀態
        int mask = Modifier.STATIC | Modifier.TRANSIENT;

        for (int i = 0; i < clFields.length; i++) {
            //註意點2: 通過位運算與(都是1才是1),判斷如果該欄位 既不是static修飾也不是transient修飾的欄位 就需要序列化
            if ((clFields[i].getModifiers() & mask) == 0) {
                list.add(new ObjectStreamField(clFields[i], false, true));
            }
        }
        int size = list.size();
        return (size == 0) ? NO_FIELDS :
            list.toArray(new ObjectStreamField[size]);
    }

怎麼自定義序列化和反序列化方法?

參考ArrayList源碼

//ArrayList中的自定義序列化方法
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    int expectedModCount = modCount;
    //註意點1:調用 ObjectOutputStream的預設 序列化方法將該序列化的欄位序列化
    s.defaultWriteObject();

    //註意點2:額外寫入數組的實際裝了多少元素(不是總容量)
    //Write out size as capacity for behavioural compatibility with clone()    
    s.writeInt(size);

    //註意點3:依次寫入數組元素
    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;

    //註意點4:調用ObjectInputStream的預設 反序列化方法將該反序列化的欄位反序列化
    s.defaultReadObject();

    //註意點5:這裡讀取的值是被忽略的
    // Read in capacity
    s.readInt(); // ignored
    
    //註意點6: 依次反序列化    
    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        int capacity = calculateCapacity(elementData, size);
        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, 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();
        }
    }
}

參考源碼註釋和補充的批註能大概理解整個流程,但是這裡有個地方比較讓我疑惑
結合註意點2,和註意點5發現ArrayList在自定義序列化方法額外寫入了size
但是反序列化時僅僅只做了讀取並沒有使用,源碼註釋也是//ignore,序列化寫入的時候也提了一下寫入size是為了相容clone()行為
參考文章https://www.zhihu.com/question/359634731 應該是版本相容問題

新的問題?為什麼寫了writeObject()方法和readObject()方法,序列化和反序列化就會按照自定義的來?

序列化反序列化自定義原理

還是結合源碼分析

//1.以下為java.io.ObjectOutputStream#writeSerialData()的源碼
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;
        //註意點1:這裡進行了是否有WriteObject方法的判定
        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);
        }
    }
}
//2.進入方法 slotDesc.hasWriteObjectMethod()
boolean hasWriteObjectMethod() {
    requireInitialized();
    //註意點2:這裡對成員變數writeObjectMethod 進行了判斷 以此為依據來確定類是否含有writeObject方法 什麼時候賦值的?(初始化)
    return (writeObjectMethod != null);
}
//3.在java.io.ObjectStreamClass.ObjectStreamClass(java.lang.Class<?>)類構造方法中 進行了初始化
private ObjectStreamClass(final Class<?> cl){
    ...    
    if(externalizable){
        cons=getExternalizableConstructor(cl);
    }else{
        cons=getSerializableConstructor(cl);
        //註意點3:這裡使用了反射機製為成員變數writeObjectMethod是否含有方法writeObject方法進行了賦值判定
        writeObjectMethod=getPrivateMethod(cl,"writeObject",
            new Class<?>[]{ObjectOutputStream.class },
            Void.TYPE);
        readObjectMethod=getPrivateMethod(cl,"readObject",
            new Class<?>[]{ObjectInputStream.class },
            Void.TYPE);
        readObjectNoDataMethod=getPrivateMethod(
            cl,"readObjectNoData",null,Void.TYPE);
        hasWriteObjectData=(writeObjectMethod!=null);
    }
    ...
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、介紹 定義: 用於定義基本操作的自定義行為 本質: 修改的是程式預設形為,就形同於在編程語言層面上做修改,屬於元編程(meta programming) 元編程(Metaprogramming,又譯超編程,是指某類電腦程式的編寫,這 ...
  • 前言 Vue3 作為一款現代的 JavaScript 框架,引入了許多新的特性和改進,其中包括 shallowRef 和 shallowReactive。這兩個功能在Vue 3中提供了更加靈活和高效的狀態管理選項,尤其適用於大型和複雜的應用程式。 Vue 3 的響應式系統 Vue3 引入了新的響應式 ...
  • 作為2024年最受歡迎的Vue.js組件庫之一,ViewDesign憑藉其現代化設計理念、強大功能和可定製性脫穎而出。這款開源UI組件庫提供了豐富的基礎組件、數據展示組件和交互反饋組件,涵蓋了大部分Web開發場景。同時,ViewDesign還具備良好的可訪問性、完善的文檔、活躍的社區支持,並對SEO... ...
  • 前言 我們每天寫vue代碼時都在用defineProps,但是你有沒有思考過下麵這些問題。為什麼defineProps不需要import導入?為什麼不能在非setup頂層使用defineProps?defineProps是如何將聲明的 props 自動暴露給模板? 舉幾個例子 我們來看幾個例子,分別 ...
  • “將抽象和實現解耦,讓它們可以獨立變化。” 橋接模式通過將一個類的抽象部分與實現部分分離開來,使它們可以獨立地進行擴展和修改。 ...
  • 我們都知道,我們寫的Java程式需要先經過編譯,生成了.class文件(位元組碼文件)。然而,電腦並不能直接解釋.class文件裡面的內容,這時候就需要一個能載入、解釋.class文件並且能按.class文件里的內容進行處理的一個東西--JVM。 JVM,就是Java虛擬機。它是一種規範,有針對不同 ...
  • 前言 池化思想在實際開發中有很多應用,指的是針對一些創建成本高,創建頻繁的對象,用完不棄,將其緩存在對象池子里,下次使用時優先從池子里獲取,如果獲取到則可以直接使用,以此降低創建對象的開銷。 我們最熟悉的資料庫連接池就是一種池化思想的應用,資料庫操作是非常頻繁的,資料庫連接的創建、銷毀開銷很大,每次 ...
  • 本文介紹基於R語言中的raster包,讀取單張或批量讀取多張柵格圖像,並對柵格圖像數據加以基本處理的方法。 1 包的安裝與導入 首先,我們需要配置好對應的R語言包;前面也提到,我們這裡選擇基於raster包來實現柵格圖像數據的讀取與處理工作。首先,如果有需要的話,我們可以先到raster包在R語言的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...