Java 串行化

Serialization 是把對象的狀態(tài)轉換為字節(jié)流,同時字節(jié)流也可以轉換為對象,反向過程叫做 Deserialization

串行化可以把對象的狀態(tài)保存到文件中,也可以通過網(wǎng)絡傳輸對象

串行化接口

java.io.Serializable接口是一個標記接口(不含有數(shù)據(jù)和方法),String和所有的原始數(shù)據(jù)類型的包裝器類都默認實現(xiàn)了該接口

ObjectOutputStream類用來串行化對象為OutputStream,類字段如果是引用,對應的引用對象也需要序列化
ObjectInputStream 類用來反序列化先前串行化的原始數(shù)據(jù)和對象,重構對象

可以寫入多個對象或原始數(shù)據(jù)類型到輸出流,這些對象必須從相應的ObjectInputstream讀取,類型和順序應該要和寫入的相同

FileOutputStream fos = new FileOutputStream("t.tmp");
ObjectOutputStream oos = new ObjectOutputStream(fos);

oos.writeInt(12345);
oos.writeObject("Today");
oos.writeObject(new Date());

oos.close();
FileInputStream fis = new FileInputStream("t.tmp");
ObjectInputStream ois = new ObjectInputStream(fis);

int i = ois.readInt();
String today = (String) ois.readObject();
Date date = (Date) ois.readObject();

ois.close();
  • 可以使用try/catch,處理轉換過程中可能出現(xiàn)的異常
  • 使用transient修飾符的字段,不會被序列化
  • 靜態(tài)字段不會序列化(serialVersionUID例外)
  • 不是每個類都可序列化,有些類是不能序列化的, 例如涉及線程的類
  • 子類實現(xiàn)Serializable接口而父類未實現(xiàn)時,父類不會被序列化
  • 父類實現(xiàn)序列化,子類自動實現(xiàn)序列化

如果需要特殊處理序列化和反序列化,可以在類中自定義序列化方法

 private void writeObject(java.io.ObjectOutputStream out)
     throws IOException
 private void readObject(java.io.ObjectInputStream in)
     throws IOException, ClassNotFoundException;
 private void readObjectNoData()
     throws ObjectStreamException;

為了避免因為,JAVA的序列化機制采用了一種特殊的算法:

1、所有保存到磁盤中的對象都有一個序列化編號
2、當程序試圖序列化一個對象時,會先檢查該對象是否已經(jīng)被序列化過,只有該對象從未(在本次虛擬機中)被序列化,系統(tǒng)才會將該對象轉換成字節(jié)序列并輸出
3、如果對象已經(jīng)被序列化,程序將直接輸出一個序列化編號,而不是重新序列化

serialVersionUID

用來確保在反序列化的過程中,加載的是同樣的類(序列號對應的類)

語法:

ANY-ACCESS-MODIFIER static final long serialVersionUID = 1L;

原因:
有可能序列化一個對象到文件中,幾個月后才在不同的JVM進行反序列化,此時對應的類可能已經(jīng)改變了。
如果要反序列化的serialVersionUID不相同,產(chǎn)生異常InvalidClassException。

生成方式:

  • 顯式聲明,比如和系統(tǒng)的版本保持一致
  • 自動生成,沒有顯式聲明的時候,進行序列化的時候根據(jù)相關規(guī)則產(chǎn)生一個默認的serialVersionUID,但是相應的計算對類的細節(jié)非常敏感,可能編譯器的實現(xiàn)而有所不同,所以強烈建議所有可序列化的類都明確聲明serialVersionUID

Externalizable

Serializable接口的子類,通過特定的兩個方法來指定要序列化的對象,而父類直接序列化所有對象

writeExternal(ObjectOutput out)
readExternal(ObjectInput in)

與父類Serializable的區(qū)別,反序列化重構對象時,先通過一個public的無參數(shù)構造函數(shù)創(chuàng)建對象,再調用readExternal方法,父類是直接通過ObjectInputStream創(chuàng)建的


測試

import java.io.*;

public class Solution {
    public static void main(String[] args) throws IOException {
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("/home/wdy/Desktop/test"));
        outputStream.writeObject(new Test(0xBBBBBBBB,"Wang"));
        outputStream.close();
    }

    public static class Test implements Serializable {
        public static final long serialVersionUID = 0xAAAAAAAAAAAAAAAAL;
        int num;
        String name;

        public Test(int num, String name) {
            this.num = num;
            this.name = name;
        }
    }
}

寫出的二進制數(shù)據(jù):

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   
00000000: AC ED 00 05 73 72 00 21 63 6F 6D 2E 67 69 74 68    ,m..sr.!com.gith
00000010: 75 62 2E 77 61 6E 67 64 79 31 32 2E 53 6F 6C 75    ub.wangdy12.Solu
00000020: 74 69 6F 6E 24 54 65 73 74 AA AA AA AA AA AA AA    tion$Test*******
00000030: AA 02 00 02 49 00 03 6E 75 6D 4C 00 04 6E 61 6D    *...I..numL..nam
00000040: 65 74 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53    et..Ljava/lang/S
00000050: 74 72 69 6E 67 3B 78 70 BB BB BB BB 74 00 04 57    tring;xp;;;;t..W
00000060: 61 6E 67                                           ang


解釋:

序列化會記錄每個類的名稱,字段的名稱和類型,最后才是具體的數(shù)據(jù)類型

ObjectOutputStream初始化時就會寫出頭信息:

  • 寫出AC ED表示魔數(shù),即文件類型,寫出版本號00 05,這兩個字段都是固定的

寫一個普通的對象writeOrdinaryObject

  • 對象標志0x73

類描述信息writeClassDesc

  • 類標志0x72,表示一個新的類描述
  • 類名稱(modified-utf-8格式,這里編碼長度為33字節(jié)即0x21,大字節(jié)序寫出為short形式,之后是UTF內(nèi)容,這里全部是單字節(jié)編碼com.github.wangdy12.Solution$Test),序列號(8個0xAA
  • 標志,一個字節(jié),表示不同的序列化類型(例如對象是否自定義了writeObject方法),這里為0x02
  • 字段數(shù)目,兩個字節(jié),這里為00 02,即兩個字段,接下來處理每個字段的描述信息
  • int字段的寫出,類型占用一個字節(jié),對應為I,即49,接下來是字段名稱,前兩個字節(jié)表示長度00 03,后面是具體的名稱num
  • String字段的寫出:簽名第一個字符對應為L,即0x4C,然后是字段名稱,前兩個字節(jié)表示長度00 04,后面是字段名稱name,如果不是原始類型,再寫出其類型簽名,這里是Ljava/lang/String;,以writeString寫出其類型簽名,先寫標志0x74,然后內(nèi)部再調用writeUTF以UTF形式寫出,長度0x12,即18,內(nèi)容Ljava/lang/String;
  • 對象塊的結束標志0x78
  • 遞歸寫出對象的父類信息描述,遞歸調用writeClassDesc,這里為null,寫出0x70

寫出具體的數(shù)據(jù)writeSerialData

  • 如果有自定義的writeObject就調用,如果沒有使用默認的defaultWriteFields寫出,先寫出原始數(shù)據(jù)類型的值,然后寫出對象字段中的實際數(shù)據(jù)

序列化使用的常量位于ObjectStreamConstants類中,內(nèi)部包含一些標志位

static final short STREAM_MAGIC = (short)0xaced;
static final short STREAM_VERSION = 5;
static final byte TC_NULL =         (byte)0x70;
static final byte TC_CLASSDESC =    (byte)0x72;
static final byte TC_OBJECT =       (byte)0x73;
static final byte TC_STRING =       (byte)0x74;
static final byte TC_ENDBLOCKDATA = (byte)0x78;

Kryo

一種更高效的的序列化方式,相同對象的序列化,大小大大減小

public class Solution {
    public static void main(String[] args) throws IOException {
        Kryo kryo = new Kryo();
        kryo.register(Test.class);//需要進行注冊,不注冊時改為 kryo.setRegistrationRequired(false);
        Test test = new Test(0xBBBBBBBB,"Wang");
        Output output = new Output(new FileOutputStream("/home/wdy/Desktop/test-kryo"));
        kryo.writeClassAndObject(output, test);
        output.close();

        Input input = new Input(new FileInputStream("/home/wdy/Desktop/test-kryo"));
        Object object2 = kryo.readClassAndObject(input);
        input.close();
        System.out.println(((Test)object2).num);
    }
}

注冊后序列化結果只有10個字節(jié),不包含類型信息,第一個字節(jié)是一個變長int,表示注冊對應的序號,之后四個字節(jié)表示Wang,且g最后一個字節(jié)的最高位為1,即最后一個字節(jié)為負數(shù),表示字符串結束,最后五個字節(jié)是一個邊長編碼的int,即0xBBBBBBBB

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   
00000000: 0B 57 61 6E E7 89 91 A2 C4 08                      .Wang.."D.

如果不進行注冊,對應結果會記錄類名稱

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   
00000000: 01 00 63 6F 6D 2E 67 69 74 68 75 62 2E 77 61 6E    ..com.github.wan
00000010: 67 64 79 31 32 2E 53 6F 6C 75 74 69 6F 6E 24 54    gdy12.Solution$T
00000020: 65 73 F4 57 61 6E E7 89 91 A2 C4 08                estWang.."D.

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • JAVA序列化機制的深入研究 對象序列化的最主要的用處就是在傳遞,和保存對象(object)的時候,保證對象的完整...
    時待吾閱讀 11,191評論 0 24
  • 一、 序列化和反序列化概念 Serialization(序列化)是一種將對象以一連串的字節(jié)描述的過程;反序列化de...
    步積閱讀 1,493評論 0 10
  • 如果你只知道實現(xiàn) Serializable 接口的對象,可以序列化為本地文件。那你最好再閱讀該篇文章,文章對序列化...
    jiangmo閱讀 561評論 0 2
  • 官方文檔理解 要使類的成員變量可以序列化和反序列化,必須實現(xiàn)Serializable接口。任何可序列化類的子類都是...
    獅_子歌歌閱讀 2,550評論 1 3
  • 你好!做夢也想不到我把信寫到五線譜上吧? 五線譜是偶然來的,你也是偶然來的。不過我給你的信值得寫在五線譜里呢.。但...
    Carina____閱讀 113評論 0 0

友情鏈接更多精彩內(nèi)容