從java的序列化和反序列化提及

從java的序列化和反序列化提及java

序列化 (Serialization)是將對象的狀態信息轉換爲能夠存儲或傳輸的形式的過程,而相反的過程就稱爲反序列化。數組

在java中容許咱們建立可複用的對象,可是這些對象僅僅存在jvm的堆內存中,有可能被垃圾回收器回收掉而消失,也可能隨着jvm的中止而消失,可是有的時候咱們但願這些對象被持久化下來,可以在須要的時候從新讀取出來。好比咱們須要在網絡中傳輸對象,首先就須要把對象序列化二進制,而後在網絡中傳輸,接收端收到這些二進制數據後進行反序列化還原成對象,完成對象的網絡傳輸,java的序列化和反序列化功能就能夠幫助咱們現實此功能。網絡

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

Serializable接口jvm

在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關鍵字修飾了,就會被過濾掉ui

readObject和writeObject方法google

在序列化過程當中,若是被序列化的類中定義了writeObject 和 readObject 方法,虛擬機會試圖調用對象類裏的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化。spa

若是沒有這樣的方法,則默認調用是 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的使用

1、整備

從protobuff官網下載protoc.exe可執行文件

2、編寫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;
}

3、編譯成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());
}
相關文章
相關標籤/搜索