Java序列化

Java序列化

參考:http://blog.csdn.net/jiangwei0910410003/article/details/18989711/
http://www.cnblogs.com/guanghuiqq/archive/2012/07/18/2597036.htmlhtml

1、Serializable接口

1. Java序列化與反序列化

  Java序列化是指把Java對象轉換爲字節序列的過程;而Java反序列化是指把字節序列恢復爲Java對象的過程。即序列化就是將Java Object轉成byte[];反序列化就是將byte[]轉成Java Object。java

2. 爲何須要序列化與反序列化

  1. 實現了數據的持久化,經過序列化能夠把數據永久地保存到文件中或者數據庫;
  2. 利用序列化實現遠程通訊,即在網絡上傳送對象的字節序列。

3. 利用Serializable接口實現序列化與反序列化

  將須要序列化的類實現Serializable接口就能夠了,Serializable接口中沒有任何方法,能夠理解爲一個標記,即代表這個類能夠序列化。算法

3.1 JDK類庫中序列化API

java.io.ObjectOutputStream:表示對象輸出流

  它的writeObject(Object obj)方法能夠對參數指定的obj對象進行序列化,把獲得的字節序列寫到一個目標輸出流中。數據庫

java.io.ObjectInputStream:表示對象輸入流

  它的readObject()方法從輸入流中讀取字節序列,再把它們反序列化成爲一個對象,並將其返回。編程

3.2 JDK類庫中序列化的步驟

  • 步驟一:建立一個對象輸出流,它能夠包裝一個其它類型的目標輸出流,如文件輸出流:ObjectOutputStream out = new ObjectOutputStream(new fileOutputStream(「D:\objectfile.obj」));
  • 步驟二:經過對象輸出流的writeObject()方法寫對象:out.writeObject(「Hello」);或者out.writeObject(new Date());

3.3 JDK類庫中反序列化的步驟

  • 步驟一:建立一個對象輸入流,它能夠包裝一個其它類型輸入流,如文件輸入流:ObjectInputStream in = new ObjectInputStream(new fileInputStream(「D:\objectfile.obj」));
  • 步驟二:經過對象輸出流的readObject()方法讀取對象:String obj1 = (String)in.readObject();或者Date obj2 = (Date)in.readObject();

  說明:爲了正確讀取數據,完成反序列化,必須保證向對象輸出流寫對象的順序與從對象輸入流中讀對象的順序一致。json

4. 例子

Student類:api

import java.io.Serializable;

public class Student implements Serializable {
    private String name;
    private char sex;
    private int year;

    public Student() {}

    public Student(String name, char sex, int year) {
        this.name = name;
        this.sex = sex;
        this.year = year;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setSex(char sex) {
        this.sex = sex;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public String getName() {
        return this.name;
    }

    public char getSex() {
        return this.sex;
    }

    public int getYear() {
        return this.year;
    }
}

測試類:數組

import java.io.*;

public class UseStudent {
    public static void main(String[] args) {
        Student st = new Student("Tom", 'M', 20);
        File file = new File("D:\\home\\IO\\student.txt");
        try {
            file.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            // Student對象序列化過程
            FileOutputStream fos = new FileOutputStream(file);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(st);
            oos.flush();
            oos.close();
            fos.close();

            // Student對象反序列化過程
            FileInputStream fis = new FileInputStream(file);
            ObjectInputStream ois = new ObjectInputStream(fis);
            Student st1 = (Student) ois.readObject();
            System.out.println("name = " + st1.getName());
            System.out.println("sex = " + st1.getSex());
            System.out.println("year = " + st1.getYear());
            ois.close();
            fis.close();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

生成的序列化文件:
緩存

5. 理解對象序列化後的文件格式

第一部分是序列化文件頭

每一個文件都是如下面這兩個字節的「魔幻數字」開始的:AC ED
後面緊跟着對象序列化格式的版本號,目前是:00 05
73:TC_OBJECT聲明這是一個新的對象安全

第二部分是序列化類的描述

72:TC_CLASSDESC聲明這裏開始一個新class
00 07:class名字的長度是7字節
53 74 75 64 65 6E 74:類名(ASCII碼:Student,注意:咱們這裏並無定義Student所屬的包,正常狀況下這裏應該是包名.Student
84 07 FA B3 1D A2 24 08: SerialVersionUID
02:標記號,該值聲明該對象支持序列化
00 03:該類所包含的域的個數爲3

第三部分是對象中各個屬性項的描述

43::域類型,表明C,表示Char類型
00 03:域名字的長度,爲3
73 65 78:sex屬性的名稱

49:域類型,表明I,表示Int類型
00 04:域名字的長度,爲4
79 65 61 72:year屬性的名稱

4C:域類型,表明L,表示String類型
00 04:域名字的長度,爲4
6E 61 6D 65:name屬性的名稱

74 00 12:實例域的類名及其長度
4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B:Ljava/lang/String;

第四部分輸出該對象父類信息描述,這裏沒有父類,若是有,則數據格式與第二部分同樣

78:TC_ENDBLOCKDATA,對象塊結束標誌
70:TC_NULL,說明沒有其餘超類的標誌

第五部分輸出對象的屬性的實際值,若是屬性項是一個對象,那麼這裏還將序列化這個對象,規則和第2部分同樣。

00 4D:M
00 00 00 14:20
74 00 03:字符串類型及其長度
54 6F 6D:Tom

  當存儲一個對象時,這個對象所屬的類也必須存儲。但不是類的全部的class信息都被存儲,只包含一些能惟一性肯定類的描述。這個類的描述包含:

  • 類名。
  • 序列化的版本唯一的ID,它是數據域類型和方法簽名的指紋。
  • 描述序列化方法的標誌集。
  • 對數據域的描述。

  指紋是經過對類、超類、接口、域類型和方法簽名按照規範方式排序,而後將安全散列算法(SHA)應用於這些數據而得到的。在讀入一個對象時,會拿其指紋與它所屬的類的當前指紋進行比對,若是它們不匹配,那麼就說明這個類的定義在該對象被寫出以後發生過變化,所以會產生一個異常。
  
總結

  • 對象流輸出中包含全部對象的類型和數據域。
  • 每一個對象都被賦予一個序列號。
  • 相同對象的重複出現將被存儲爲對這個對象的序列號的引用。
  • 序列化的流只記錄了對象所屬類的一些定義和域的值,其寫入和讀取是分別由客戶端程序和服務端程序組裝完成的,若是雙方沒有一個共同的基礎(同一個類),是沒法完成的。

6. serialVersionUID

  簡單來講,Java的序列化機制是經過在運行時判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID與本地相應實體(類)的serialVersionUID進行比較,若是相同就認爲是一致的,能夠進行反序列化,不然就會出現序列化版本不一致的異常(InvalidCastException)。

serialVersionUID的生成方式: 根據類名、接口名、成員方法及屬性等來生成一個64位(8字節的long型)的哈希字段,好比:private static final long serialVersionUID = xxxxL;

  當實現java.io.Serializable接口的實體(類)沒有顯式地定義一個名爲serialVersionUID(類型爲long的變量)時,Java序列化機制會根據編譯的class(根據類名、接口名、成員方法及屬性等來生成一個64位的哈希字段,理論上是一一映射的關係,也就是惟一的)自動生成一個serialVersionUID做序列化版本,這種狀況下,若是class文件(類名,方法名等)沒有發生變化,就算再編譯屢次,serialVersionUID也不會變化的。若是咱們不但願經過編譯來強制劃分軟件版本,就須要顯式地定義一個名爲serialVersionUID,類型爲long的變量,不修改這個變量值的序列化實體均可以相互進行序列化和反序列化。

咱們考慮如下幾個問題:

一、問題一:假設有A端和B端,若是2處的serialVersionUID不一致,會產生什麼錯誤呢?
答:會報錯版本不一致(解答能夠見下面的測試)。

二、問題二:假設2處serialVersionUID一致,若是A端增長一個字段,B端不變,(或者A端不變,B端減小一個字段)會是什麼狀況呢?
答:序列化,反序列化正常,A端增長的字段丟失(被B端忽略)。

三、問題三:假設2處serialVersionUID一致,若是B段增長一個字段,A端不變,(或者A端減小一個字段,B端不變)會是什麼狀況呢?
答:序列化,反序列化正常,B端新增長的int字段被賦予了默認值0。

  強烈建議全部可序列化類都顯式聲明 serialVersionUID 值,緣由是計算默認的 serialVersionUID 對類的詳細信息具備較高的敏感性,根據編譯器實現的不一樣可能千差萬別,這樣在反序列化過程當中可能會致使意外的 InvalidClassException。還強烈建議使用 private 修飾符顯示聲明 serialVersionUID(若是可能),緣由是:serialVersionUID 字段不該該對子類起做用。數組類不能聲明一個明確的 serialVersionUID,所以它們老是具備默認的計算值,可是數組類沒有匹配 serialVersionUID 值的要求。

下面展開測試

有兩個包,模擬A端和B端,分別有本身的Student實體類。測試在A端序列化,在B端反序列化。程序結構:

A包:

package A;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerialA {
    public static void main(String[] args) {
        Student student = new Student(1, "song");
        System.out.println("Object SeStudent" + student);
        try {
            File file = new File("D:\\home\\IO\\student.txt");
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
            FileOutputStream fos = new FileOutputStream(file);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(student);
            oos.flush();
            oos.close();
            fos.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
package A;

import java.io.Serializable;

public class Student implements Serializable {
    private static final long serialVersionUID = 6977402643848374753L;
    int id;
    String name;

    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public String toString() {
        return "DATA: " + id + " " + name;
    }
}

B包:

package B;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;

public class SerialB {
    public static void main(String[] args) {
        Student student;
        try {
            File file = new File("D:\\home\\IO\\student.txt");
            FileInputStream fis = new FileInputStream(file);
            ObjectInputStream ois = new ObjectInputStream(fis);
            student = (Student) ois.readObject();
            ois.close();
            fis.close();
            System.out.println("Object DeStudent" + student);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
package B;

import java.io.Serializable;

public class Student implements Serializable {
    private static final long serialVersionUID = 6977402643848374753L;
    int id;
    String name;

    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public String toString() {
        return "DATA: " + id + " " + name;
    }
}

  咱們在A端序列化,B端反序列化,獲得:

也就是利用不一樣包下的類,是不能完成反序列化的

修改測試程序結構以下,程序代碼不變:

  咱們在A端序列化,B端反序列化,獲得:

問題1:假設有A端和B端,若是2處的serialVersionUID不一致,會產生什麼錯誤呢?

  咱們首先在A端進行序列化,而後在B端反序列化以前,將Student.java中的serialVersionUID改成:
private static final long serialVersionUID = 1L;
  獲得:

java.io.InvalidClassException: model.Student; local class incompatible: stream classdesc serialVersionUID = 6977402643848374753, local class serialVersionUID = 1
    at java.io.ObjectStreamClass.initNonProxy(Unknown Source)
    at java.io.ObjectInputStream.readNonProxyDesc(Unknown Source)
    at java.io.ObjectInputStream.readClassDesc(Unknown Source)
    at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
    at java.io.ObjectInputStream.readObject0(Unknown Source)
    at java.io.ObjectInputStream.readObject(Unknown Source)
    at B.SerialB.main(SerialB.java:17)

問題2:假設2處serialVersionUID一致,若是A端增長一個字段,B端不變,(或者A端不變,B端減小一個字段)會是什麼狀況呢?

  咱們首先在A端進行序列化,而後在B端反序列化以前,將Student.java改成:

  獲得:

Object DeStudentDATA: 1

問題3:假設2處serialVersionUID一致,若是B段增長一個字段,A端不變,(或者A端減小一個字段,B端不變)會是什麼狀況呢?

  咱們首先在A端進行序列化,而後在B端反序列化以前,將Student.java改成:

  獲得:

Object DeStudentDATA: 1 song 0

7. 其餘注意點

Serializable接口序列化屬於深複製

  反序列化還原後的對象地址與原來的的地址不一樣。即直接將對象序列化到輸出流中,而後將其讀回,這樣產生的新對象是對現有對象的一個深拷貝或者深複製。可是注意:使用這種方式深複製雖然比較方便,可是他比顯式的構建新對象並賦值數據域慢不少。

transient關鍵字

  標記某些數據域是不能夠序列化的。當某個字段被聲明爲transient後,默認序列化機制就會忽略該字段。transient關鍵字只能修飾變量,而不能修飾方法和類。注意,本地變量是不能被transient關鍵字修飾的。變量若是是用戶自定義類變量,則該類須要實現Serializable接口。

靜態變量不能序列化

  序列化會忽略靜態變量,即序列化不保存靜態變量的狀態。靜態成員屬於類級別的,因此不能序列化。即 序列化的是對象的狀態不是類的狀態。這裏的不能序列化的意思,是序列化信息中不包含這個靜態成員域。因爲該值是JVM加載該類時分配的值,因此讀取該對象時這個值依然存在。

當父類繼承Serializable接口時,全部子類均可以被序列化;若是序列化的屬性是對象,則這個對象也必須實現Serializable接口,不然會報錯;當一個對象的實例變量引用其餘對象,序列化該對象時,也把引用對象進行序列化;static,transient後的變量不能被序列化

2、實現序列化的其它方式

  若是僅僅只是讓某個類實現Serializable接口,而沒有其它任何處理的話,就是使用默認序列化機制。使用默認機制,在序列化對象時,不只會序列化當前對象自己,還會對該對象引用的其它對象也進行序列化,一樣地,這些其它對象引用的另外對象也將被序列化,以此類推。因此,若是一個對象包含的成員變量是容器類對象,而這些容器所含有的元素也是容器類對象,那麼這個序列化的過程就會較複雜,開銷也較大,因此咱們介紹兩種比較快速的序列化方式。

代碼

實體類:

import java.io.Serializable;

public class Student implements Serializable {
    private String name;
    private char sex;
    private int year;

    public Student() {}

    public Student(String name, char sex, int year) {
        this.name = name;
        this.sex = sex;
        this.year = year;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setSex(char sex) {
        this.sex = sex;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public String getName() {
        return this.name;
    }

    public char getSex() {
        return this.sex;
    }

    public int getYear() {
        return this.year;
    }
}

序列化類:

import com.alibaba.fastjson.JSON;
import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtobufIOUtil;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import com.test.serializationTest.model.Student;

import java.io.*;

public class Test {
    private Student st = new Student("Tom", 'M', 20);
    public void testSerializable() {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        try {
            //序列化
            ObjectOutputStream oos = new ObjectOutputStream(os);
            oos.writeObject(st);
            oos.flush();
            oos.close();//只是爲了方便簡潔的作個例子,真實編程要放到finally下
            System.out.println("Serializable字節數:"+os.toByteArray().length);
            System.out.println("Serializable字節流:"+os);

            //反序列化
            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(os.toByteArray()));
            Student st2  = (Student) ois.readObject();
            System.out.println("Serializable反序列化:"+st2.getName()+st2.getSex()+st2.getYear());
            ois.close();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void testJSON() {
        String json = JSON.toJSONString(st);
        System.out.println("JSON字節數::"+json.getBytes().length);
        System.out.println("JSON字符串:"+json);
        Student st2 = JSON.parseObject(json,Student.class);
        System.out.println("JSON反序列化:"+st2.getName()+st2.getSex()+st2.getYear());
    }

    public void testProtostuff() {
        RuntimeSchema<Student> schema = RuntimeSchema.createFrom(Student.class);
        //序列化
        byte[] data = ProtobufIOUtil.toByteArray(st, schema, LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
        System.out.println("Protostuff字節數::"+data.length);
        System.out.println("Protostuff字節流:"+data);

        //反序列化
        Student st2 = schema.newMessage();
        ProtostuffIOUtil.mergeFrom(data,st2, schema);
        System.out.println("Protostuff反序列化:"+st2.getName()+st2.getSex()+st2.getYear());
    }

測試類:

public class SerializationTestApplication {
    public static void main(String[] args) {
        Test test = new Test();
        test.testSerializable();
        test.testJSON();
        test.testProtostuff();
    }
}

輸出:

Serializable字節數:114
Serializable字節流:�� sr (com.test.serializationTest.model.Student=;S^��Y C sexI yearL namet Ljava/lang/String;xp M   t Tom
Serializable反序列化:TomM20
JSON字節數::34
JSON字符串:{"name":"Tom","sex":"M","year":20}
JSON反序列化:TomM20
Protostuff字節數::9
Protostuff字節流:[B@2b9ed6da
Protostuff反序列化:TomM20

Java的序列化:
優勢:使用方便,可序列化全部類,不須要提供第三方的類庫。
缺點:速度慢,沒法跨語言,字節數佔用比較大(徹底利用反射實現序列化和反序列化,而且保存了關於類的一些信息)。

Protostuff

  Protostuff是一個開源的、基於Java語言的序列化庫。Protostuff支持的序列化格式包括:json;xml;POJO(能夠理解爲只包括屬性和對應的set/get方法的實體類)...。
  典型應用見上面的代碼。

原理

  
如上圖所示,將對象中的屬性和值按照tag和value的形式存儲,其中tag表示的相似於屬性編號(爲了最小化空間),value表示的是屬性的值,因此若是類中的屬性順序變化,會致使反序列化不正確。

優勢:速度快;佔用字節數很小(相比Java的序列化縮小10倍以上),適合網絡傳輸節省io;跨語言 。
缺點:反序列化時需用戶本身初始化序列化後的對象,Protostuff負責將該對象進行賦值;依賴於第三方jar類庫;二進制格式致使可讀性差;而且支持的格式比較少,比較有用的僅僅是POJO的序列化。

爲何Protostuff速度快

  1. Java序列化和反序列化是利用反射,不須要經過屬性的get,set方法;而Protostuff經過反射解析類文件後,能夠進行緩存,而後經過set和get方法進行序列化和反序列化(反序列化時只是賦值操做)。
  2. Java序列化時類型必須徹底匹配(全路徑類名+序列化id);Protostuff反序列化時並不要求類型匹配,好比包名、類名甚至是字段名,它僅僅須要序列化類型A 和反序列化類型B 的字段類型可轉換(好比int能夠轉換爲long),而且按照編號能夠對應便可。

阿里巴巴的FastJson

  Fastjson是一個Java語言編寫的高性能的JSON處理器,由阿里巴巴公司開發。無依賴,不須要例外額外的jar,可以直接跑在JDK上。FastJson採用首創的算法,將parse的速度提高到極致,超過全部json庫。
  嚴格意義上JSON並非序列化技術,它是將對象包裝成JSON字符串傳輸,格式相似於鍵值對形式,{"name":"Tom","sex":"M","year":20},因爲包含有多餘的括號,引號,冒號等,序列化後包含34字節,可是相比於Java的序列化,佔用字節數減少了三倍以上。 優勢:明文結構一目瞭然,能夠跨語言,屬性的增長減小對解析端影響較小,支持類型比Protostuff多。 缺點:字節數較多,依賴於不一樣的第三方類庫。

相關文章
相關標籤/搜索