從java的序列化和反序列化說起

楊輝發表於2019-01-19

從java的序列化和反序列化說起

序列化 (Serialization)是將物件的狀態資訊轉換為可以儲存或傳輸的形式的過程,而相反的過程就稱為反序列化。

在java中允許我們建立可複用的物件,但是這些物件僅僅存在jvm的堆記憶體中,有可能被垃圾回收器回收掉而消失,也可能隨著jvm的停止而消失,但是有的時候我們希望這些物件被持久化下來,能夠在需要的時候重新讀取出來。比如我們需要在網路中傳輸物件,首先就需要把物件序列化二進位制,然後在網路中傳輸,接收端收到這些二進位制資料後進行反序列化還原成物件,完成物件的網路傳輸,java的序列化和反序列化功能就可以幫助我們現實此功能。

那麼java要怎麼樣才能實現序列化和反序列化呢?

Serializable介面

在java中要實現序列化和和反序列化只需要實現Serializable介面,任何檢視將沒有實現此介面的物件進行序列化和反序列化操作都會丟擲NotSerializableException,下面是實現:

public <T> byte[] serializer(T obj) {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = null;
    try {
        oos = new ObjectOutputStream(baos);
        oos.writeObject(obj);
    } catch (IOException e) {
        logger.error("java序列化發生異常:{}",e);
        throw new RuntimeException(e);
    }finally{
        try {
            if(oos != null)oos.close();
        } catch (IOException e) {
            logger.error("java序列化發生異常:{}",e);
        }
    }
    return baos.toByteArray();
}

public <T> T deserializer(byte[] data, Class<T> clazz) {
    ByteArrayInputStream bais = new ByteArrayInputStream(data);
    ObjectInputStream ois = null;
    try {
        ois = new ObjectInputStream(bais);
        return (T)ois.readObject();
    } catch (Exception e) {
        logger.error("java反序列化發生異常:{}",e);
        throw new RuntimeException(e);
    }finally{
        try {
            ois.close();
        } catch (IOException e) {
            logger.error("java反序列化發生異常:{}",e);
            throw new RuntimeException(e);
        }
    }
}

transient關鍵字

正常情況下,在序列化過程中,物件裡面的屬性都會被序列化,但是有的時候,我們想過濾掉某個屬性不要被序列化,該怎麼辦呢,很簡單java給我們提供了一個關鍵字來實現:transient,只要被transient關鍵字修飾了,就會被過濾掉

readObject和writeObject方法

在序列化過程中,如果被序列化的類中定義了writeObject 和 readObject 方法,虛擬機器會試圖呼叫物件類裡的 writeObject 和 readObject 方法,進行使用者自定義的序列化和反序列化。

如果沒有這樣的方法,則預設呼叫是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。

使用者自定義的 writeObject 和 readObject 方法可以允許使用者控制序列化的過程,比如可以在序列化的過程中動態改變序列化的數值。

細心的你肯定也發現了,我們在序列化的類裡面定於了這兩個方法,但是並沒有顯式的呼叫這兩個方法,那到底是誰呼叫的,又是何時被呼叫的呢?

深入ByteArrayOutputStream類原始碼會發現其呼叫棧:

ObjectOutputStream.writeObject(Object obj)———–>writeObject0(Object obj, boolean unshared)———–>writeOrdinaryObject(Object obj,ObjectStreamClass desc,boolean unshared)———–>writeSerialData(Object obj, ObjectStreamClass desc)

在writeSerialData方法裡面會先獲取序列化類裡面是否有writeObject(ObjectOutputStream out),有就會反射的呼叫,沒有就執行預設的序列化方法defaultWriteFields(obj, slotDesc)。

ByteArrayInputStream也是同樣的原理。

如果您讀過在ArrayList的原始碼,你可能會發現在ArrayList中的欄位elementData被關鍵字transient修飾了,而elementData欄位是ArrayList儲存元素的,難道ArrayList儲存的元素序列化會被忽略嗎?但是你會發現並沒有被忽略,而是能正常的序列化和反序列化,這是為什麼呢?答案就是,ArrayList寫有上面提到的readObject和writeObject兩個方法,ArrayList實際上是動態陣列,每次在放滿以後自動增長設定的長度值,如果陣列自動增長長度設為50,而實際只放了1個元素,那就會序列化49個null元素。為了保證在序列化的時候不會將這麼多null同時進行序列化,ArrayList把元素陣列設定為transient,自定義序列化過程,這樣可以優化儲存。

Externalizable介面

除了Serializable 之外,java中還提供了另一個序列化介面Externalizable,繼承了Serializable,該介面中定義了兩個抽象方法:writeExternal()與readExternal()。當使用Externalizable介面來進行序列化與反序列化的時候需要開發人員重寫writeExternal()與readExternal()方法。

序列化ID

虛擬機器是否允許反序列化,不僅取決於類路徑和功能程式碼是否一致,一個非常重要的一點是兩個類的序列化 ID 是否一致(就是 private static final long serialVersionUID)

序列化 ID 在 Eclipse 下提供了兩種生成策略,一個是固定的 1L,一個是隨機生成一個不重複的 long 型別資料(實際上是使用 JDK 工具生成),在這裡有一個建議,如果沒有特殊需求,就是用預設的 1L 就可以,這樣可以確保程式碼一致時反序列化成功。那麼隨機生成的序列化 ID 有什麼作用呢,有些時候,通過改變序列化 ID 可以用來限制某些使用者的使用。

Protobuf

我們知道java自帶的序列化效率是非常低的,因為它序列化生成的位元組數非常多(包含了很多類的資訊),不太適合用於儲存和在網路上傳輸,下面來介紹下google給我們提供一個序列化效率相當高的框架protobuff,比起java原生的序列化出來的位元組數小十幾倍。那麼它是如何做到的呢?

以int型別為例,int在java的佔用4個位元組,如果我們不做特殊處理,int型別的值轉化成二進位制也需要佔用4個位元組的空間,但是protobuff卻不是這樣做的,請看下面程式碼:

while (true) {
    if ((value & ~0x7F) == 0) {
        UnsafeUtil.putByte(buffer, position++, (byte) value);
        break;
    } else {
        UnsafeUtil.putByte(buffer, position++, (byte) ((value & 0x7F) | 0x80));
        value >>>= 7;
    }
}

value & ~0x7F 是什麼意思呢?0x7F取反跟value相與,那麼value的低7位全部被置0了,如果此時相與的值等於0,說明value的值不會大於0x7F=127,就可以用一個位元組來表示,大大節省了位元組數,看個列子:

value=0x00000067,轉換成二進位制:

0000 0000  0000 0000  0000 0000  0110 0111

& 1111 1111 1111 1111 1111 1111 1000 0000

= 0000 0000 0000 0000 0000 0000 0000 0000

此時value & ~0x7F=0,當把value強制轉換成byte型別時,int會被截斷,只剩下低位位元組,於是當value值小於128時,序列化後的位元組就變成:0110 0111,一個位元組就可以表示了。

問題來了,如果value的值大於0x7F呢,接著看(value & 0x7F) | 0x80這句程式碼,假設value=2240,

 0000 0000  0000 0000  0000 1000  1100 0000     0x000008C0

& 0000 0000 0000 0000 0000 0000 0111 1111 0x0000007F

= 0000 0000 0000 0000 0000 0000 1100 0000 0x000000C0

| 0000 0000 0000 0000 0000 0000 1000 0000 0x00000080

= 0000 0000 0000 0000 0000 0000 1100 0000 0x000000C0

這個過程意思就是獲取value的最低位位元組,把這個位元組的最高位置為1,表示後面還有可讀位元組。

對0x000000C0強轉byte型別就變成:1100 0000,然後向右移7位:

0000 0000 0000 0000 0000 0000 0001 0001

重複上面的步驟,得到0001 0001,迴圈結束,最後得到:

1100 0000 0001 0001

2個位元組就可以表示2240了,但是此時你會發現我們每次向右移動的是7位,移4次才能表示28位,但是int要佔用32位,如果value的值比較大,假如等於2147483647,那麼這是就需要5個位元組來表示,綜上所述protobuff表示一個int型別的值就不會固定4個位元組,而是用1-5個位元組動態來表示;那麼你可能又會有疑問了,5個位元組來表示一個int,位元組數不是變多了麼?其實從概率角度來看,我們業務上不能可能每一個int值都是一個非常大的值,所以還是可以為我們節省非常大的位元組空間。同理long,double,float也是同樣的原理。下面就以proptobuff3來介紹下protobuff的使用

一、整備

從protobuff官網下載protoc.exe可執行檔案

二、編寫proto檔案,具體的語法參見官網文件

syntax = "proto3";
option java_package = "com.yanghui.serialize.protobuf3";
option java_outer_classname = "PersonModule";
message Person {
    int32 age = 1;
    int64 time = 2;
    string name = 3;
    map<string,string> properties = 4;
}

三、編譯成java類

e:/study/protobuf/bin/protoc.exe -I=D:/workspace/serialize/src/main/java/com/yanghui/serialize/protobuf3 --java_out=D:/workspace/serialize/src/main/java person.proto

-I:表示proto檔案所在目錄

–java_out:表示輸出java的類

執行以上命令就會在指定的目錄生成一個java類PersonModule.java,接下來就可以使用了

@Test
public void testProtobuffSerialize() throws InvalidProtocolBufferException {
    Builder builder = PersonModule.Person.newBuilder();
    builder.setAge(21);
    builder.setTime(100L);
    builder.setName("yanghui");
    builder.putProperties("key1", "value1");
    com.yanghui.serialize.protobuf3.PersonModule.Person person = builder.build();

    byte[] personBytes = person.toByteArray();
    System.out.println(Arrays.toString(personBytes));
    System.out.println(personBytes.length);

    com.yanghui.serialize.protobuf3.PersonModule.Person p = 
        com.yanghui.serialize.protobuf3.PersonModule.Person.parseFrom(personBytes);
    System.out.println(p.toString());
}


相關文章