Java序列化與反序列化

java中將對象編碼爲字節流稱之爲序列化,反之將字節流重建成對象稱之爲反序列化。


 序列化主要用途:(1)把對象的字節序列永久地保存到文件中; (2)在網絡上傳送對象的字節序列;

當兩個進程在進行遠程通信時,彼此可以發送各種類型的數據。無論是何種類型的數據,都會以二進制序列的形式在網絡上傳送。
發送方需要把這個Java對象轉換爲字節序列,才能在網絡上傳送;接收方則需要把字節序列再恢復爲Java對象。


 什麼情況下需要序列化:

  • 當你想把的內存中的對象狀態保存到一個文件中或者數據庫中時候;
  • 當你想用套接字在網絡上傳送對象的時候;
  • 當你想通過RMI傳輸對象的時候;

序列化的幾種方式
      比較常見的做法有兩種:一是把對象包裝成JSON字符串傳輸,二是採用java對象的序列化和反序列化。隨着Google工具protoBuf的開源,protobuf也是個不錯的選擇。

舉例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.io.Serializable;
 
public class Person implements Serializable {
    private String name;
    private int age;
 
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    public String getName() {
        return name;
    }
 
    public int getAge() {
        return age;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
}

序列化/反序列化:

複製代碼
import com.serializable.Person;
import org.junit.Test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class TestSerializable {
    @Test
    public void testSeria() throws Exception {
        File file = new File("person.txt");
        Person lufei = new Person("路飛", 18);
        ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(file));
        os.writeObject(lufei);
        os.close();
        ObjectInputStream oi = new ObjectInputStream(new FileInputStream(file));
        Object newPerson = oi.readObject();
        oi.close();
        System.out.println(newPerson);
    }
}
複製代碼

運行結果:

person.txt中的內容使用utf-8編碼之後:
��srcom.serializable.Personk�K�?�IageLnametLjava/lang/String;xpt路飛

反序列化後:
com.serializable.Person@6daa8eb7

反序列化對象:

序列化注意事項:

  • 一個類的序列化要通過實現Serializable接口來實現。如果沒有實現這個接口,則無法實現序列化和反序列化,實現序列化的類的子類也可以實現序列化。
  • 子類實現了序列化接口,但是父類沒有實現序列化接口的話,父類必須要實現一個無參數構造器,否則會拋異常。
  • 運行過程中吐過遇到沒實現序列化接口的類會拋出NotSerializableException異常。    
  • writeReplace() 方法可以使對象被寫入到流之前,用一個對象來替換自己。
  • java.io.ObjectOutputStream代表對象輸出流,它的writeObject(Object obj)方法可對參數指定的obj對象進行序列化,把得到的字節序列寫到一個目標輸出流中。 如果對象包含其他對象的引用,則writeObject()方法遞歸序列化這些對象。
  • java.io.ObjectInputStream代表對象輸入流,它的readObject()方法從一個源輸入流中讀取字節序列,再把它們反序列化爲一個對象。
  • 當一個對象的實例變量引用其他對象,序列化該對象時也把引用對象進行序列化。
  • Java 序列化機制爲了節省磁盤空間,具有特定的存儲規則,當寫入文件的爲同一對象時,並不會再將對象的內容進行存儲,而只是再次存儲一份引用,增加一些存儲空間來表示新增引用和一些控制信息的空間。

 一致性/兼容性:

通過在運行時判斷serialVersionUID來檢查版本是否具有一致性。在進行反序列化時,JVM會把字節流中serialVersionUID與本地實體類的serialVersionUID進行比較, 如果相同則是認爲一致的,否則就會拋出InvalidClassException異常。


serialVersionUID

private static final long serialVersionUID;

該靜態變量在反序列化過程中用於驗證序列化對象的發送者和接收者是否爲該對象加載了與序列化兼容的類。如果接收者的serialVersionUID與對應的發送者版本號不同,則拋出InvalidClassException異常。

有兩種生成方式:
       一個是默認的1L,比如:private static final long serialVersionUID = 1L;
       一個是根據類名、接口名、成員方法及屬性等來生成一個64位的哈希字段,比如: private static final long serialVersionUID = xxxxL;

如果可序列化類未顯式聲明 serialVersionUID,則運行時將根據該類計算一個默認的serialVersionUID 值。不過,強烈建議所有可序列化類都顯式聲明 serialVersionUID 值,原因是計算默認的 serialVersionUID根據編譯器實現的不同可能千差萬別,這樣在反序列化過程中可能會導致意外的 InvalidClassException。

顯式地定義serialVersionUID有兩種用途:
    (1)在某些場合,希望類的不同版本對序列化兼容,因此需要確保類的不同版本具有相同的serialVersionUID;在某些場合,不希望類的不同版本對序列化兼容,因此需要確保類的不同版本具有不同的serialVersionUID。
    (2)當你序列化了一個類實例後,希望更改一個字段或添加一個字段,不設置serialVersionUID,所做的任何更改都將導致無法反序化舊有實例,並在反序列化時拋出一個異常。如果你添加了serialVersionUID,在反序列舊有實例時,新添加或更改的字段值將設爲初始化值(對象爲null,基本類型爲相應的初始默認值),字段被刪除將不設置。


 

序列化時,類的所有數據成員應可序列化除了聲明爲transient或static的成員。
      使用 Transient 關鍵字可以使得字段不被序列化,那麼還有別的方法嗎?根據父類對象序列化的規則,我們可以將不需要被序列化的字段抽取出來放到父類中,子類實現 Serializable 接口,父類不實現,根據父類序列化規則,父類的字段數據將不被序列化。

transient舉例:

測試類:

運行結果:


對敏感字段加密       

情境:服務器端給客戶端發送序列化對象數據,對象中有一些數據是敏感的,比如密碼字符串等,希望對該密碼字段在序列化時,進行加密,而客戶端如果擁有解密的密鑰,只有在客戶端進行反序列化時,纔可以對密碼進行讀取,這樣可以一定程度保證序列化對象的數據安全。
       解決:在序列化過程中,虛擬機會試圖調用對象類裏的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化,如果沒有這樣的方法,則默認調用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程,比如可以在序列化的過程中動態改變序列化的數值。基於這個原理,可以在實際應用中得到使用,用於敏感字段的加密工作。

舉例:

複製代碼
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectInputStream.GetField;
import java.io.ObjectOutputStream;
import java.io.ObjectOutputStream.PutField;
import java.io.Serializable;

public class Person2 implements Serializable {
    private String password = "pass";

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    private void writeObject(ObjectOutputStream out) {
        try {
            PutField putFields = out.putFields();
            System.out.println("原密碼:" + password);
            password = "new password";//模擬加密
            putFields.put("password", password);
            System.out.println("加密後的密碼" + password);
            out.writeFields();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void readObject(ObjectInputStream in) {
        try {
            GetField readFields = in.readFields();
            Object object = readFields.get("password", "");
            System.out.println("要解密的字符串:" + object.toString());
            password = "pass";//模擬解密,需要獲得本地的密鑰
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        try {
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
            out.writeObject(new Person2());
            out.close();

            ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj"));
            Person2 t = (Person2) oin.readObject();
            System.out.println("解密後的字符串:" + t.getPassword());
            oin.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

運行結果:

原密碼:pass
加密後的密碼new password
要解密的字符串:new password
解密後的字符串:pass

說明:writeObject 方法中,對密碼進行了加密,在 readObject 中則對 password 進行解密,只有擁有密鑰的客戶端,纔可以正確的解析出密碼,確保了數據的安全。


 

 可能會遇到的問題:

1)多個引用寫入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
   public void testSeria() throws Exception {
       File file = new File("person.txt");
       Person lufei = new Person("路飛"18);
       ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(file));
       os.writeObject(lufei);
       lufei.setAge(20);
       os.writeObject(lufei);
       os.close();
 
       ObjectInputStream oi = new ObjectInputStream(new FileInputStream(file));
       Person lufei1 = (Person) oi.readObject();
       Person lufei2 = (Person) oi.readObject();
       oi.close();
       System.out.println(lufei1.getAge());
       System.out.println(lufei2.getAge());
   }  

輸出結果:

18
18

分析:在默認情況下,對於一個實例的多個引用,只會寫入一次。可以通過ObjectOutputStream的rest方法或着writeUnshared方法實現多次寫入。

1
2
3
4
5
os.writeObject(lufei);
lufei.setAge(20);
os.reset();
os.writeObject(lufei);
os.close();

輸出結果:

18
20


2)類中字段修改 

複製代碼
import java.io.Serializable;

public class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

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

    public void setAge(int age) {
        this.age = age;
    }
}
複製代碼

如果反序列化中的Person類中添加了新的屬性,private int weight; 表示體重

複製代碼
@Test
    public void testSeria() throws Exception {
        File file = new File("person.txt");
        Person lufei = new Person("路飛", 18);
        ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(file));
        os.writeObject(lufei);
        os.close();

        ObjectInputStream oi = new ObjectInputStream(new FileInputStream(file));
        Person lufei1 = (Person) oi.readObject();
        oi.close();
        System.out.println("weight="+lufei1.getWeight());
    }
複製代碼

運行結果:

weight=0


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章