1. 程式人生 > 實用技巧 >Java物件的序列化和反序列化

Java物件的序列化和反序列化

序列化的定義

序列化:把物件轉化為可傳輸的位元組序列過程稱為序列化。

反序列化:把位元組序列還原為物件的過程稱為反序列化。


為什麼要序列化?

如果光看定義我想你很難一下子理解序列化的意義,那麼我們可以從另一個角度來推匯出什麼是序列化, 那麼究竟序列化的目的是什麼?

其實序列化最終的目的是為了物件可以跨平臺儲存,和進行網路傳輸。而我們進行跨平臺儲存和網路傳輸的方式就是IO,而我們的IO支援的資料格式就是位元組陣列。

因為我們單方面的只把物件轉成位元組陣列還不行,因為沒有規則的位元組陣列我們是沒辦法把物件的本來面目還原回來的,所以我們必須在把物件轉成位元組陣列的時候就制定一種規則(序列化),那麼我們從IO流裡面讀出資料的時候再以這種規則把物件還原回來(反序列化)。

如果我們要把一棟房子從一個地方運輸到另一個地方去,序列化就是我把房子拆成一個個的磚塊放到車子裡,然後留下一張房子原來結構的圖紙,反序列化就是我們把房子運輸到了目的地以後,根據圖紙把一塊塊磚頭還原成房子原來面目的過程


什麼情況下需要序列化?

通過上面我想你已經知道了凡是需要進行“跨平臺儲存”和”網路傳輸”的資料,都需要進行序列化。

本質上儲存和網路傳輸 都需要經過 把一個物件狀態儲存成一種跨平臺識別的位元組格式,然後其他的平臺才可以通過位元組資訊解析還原物件資訊。


序列化的方式

序列化只是一種拆裝組裝物件的規則,那麼這種規則肯定也可能有多種多樣,比如現在常見的序列化方式有:

JDK(不支援跨語言)、JSON、XML、Hessian、Kryo(不支援跨語言)、Thrift、Protostuff、FST(不支援跨語言)


序列化技術選型的幾個關鍵點

序列化協議各有千秋,不能簡單的說一種序列化協議是最好的,只能從你的當時環境下去選擇最適合你們的序列化協議,如果你要為你的公司專案進行序列化技術的選型,那麼主要從以下幾個因素。

協議是否支援跨平臺

如果你們公司有好多種語言進行混合開發,那麼就肯定不適合用有語言侷限性的序列化協議,要不然你JDK序列化出來的格式,其他語言並沒法支援。

序列化的速度

如果序列化的頻率非常高,那麼選擇序列化速度快的協議會為你的系統性能提升不少。

序列化出來的大小

如果頻繁的在網路中傳輸的資料那就需要資料越小越好,小的資料傳輸快,也不佔頻寬,也能整體提升系統的效能。


Java 是如何實現序列化的?

前面主要介紹了一下什麼是序列化,那麼下面主要講下JAVA是如何進行序列化的,以及序列化的過程中需要注意的一些問題

  • java 實現序列化很簡單,只需要實現Serializable 介面即可。

    public class User implements Serializable{
     //年齡
     private int age;
     //名字
     private String name ;
    
     public int getAge() {
     return age;
        }
     public void setAge(int age) {
     this.age = age;
        }
    
     public String getName() {
     return name;
        }
    
     public void setName(String name) {
     this.name = name;
        }
    }
    
  • 把User物件設定值後寫入檔案

    FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    
    User user = new User();
    user.setAge(18);
    user.setName("sandy");
    oos.writeObject(user);
    
    oos.flush();
    oos.close();
    
  • 再把從檔案讀取出來的轉換為物件

    FileInputStream fis = new FileInputStream("D:\\temp.txt");
    
    ObjectInputStream oin = new ObjectInputStream(fis);
    
    User user = (User) oin.readObject();
    
    System.out.println("name="+user.getName());
    

輸出結果為:name=sandy

以上把User物件進行二進位制的資料儲存後,並從檔案中讀取資料出來轉成User物件就是一個序列化和反序列化的過程。

序列化原理推薦閱讀:http://developer.51cto.com/art/200908/147650.htm


JAVA序列化中常見的問題

  • 問題一:static 屬性不能被序列化

原因:序列化儲存的是物件的狀態,靜態變數屬於類的狀態,因此 序列化並不儲存靜態變數。

  • 問題二:Transient 屬性不會被序列化

接著上面的案例,我們在User裡面加上一個transient 狀態的心情屬性mood;

public class User implements Serializable {
 //年齡
 private int age;
 //名字
 private String name;
 //心情
 private transient String mood;
//省略get set方法

}

把User物件設定值後寫入檔案

FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);

User user = new User();
user.setMood("愉快");
oos.writeObject(user);

oos.flush();
oos.close();

再把從檔案讀取出來的轉換為物件並列印mood的值

FileInputStream fis = new FileInputStream("D:\\temp.txt");

ObjectInputStream oin = new ObjectInputStream(fis);

User user1 = (User) oin.readObject();

System.out.println("mood="+user1.getMood());

輸出結果為:mood=null(原生型別為對應型別的預設值,包裝型別為null)

  • 問題三:序列化版本號serialVersionUID

所有實現序列化的物件都必須要有個版本號,這個版本號可以由我們自己定義,當我們沒定義的時候JDK工具會按照我們物件的屬性生成一個對應的版本號。


版本號有什麼用?

其實這個版本號就和我們平常軟體的版本號一樣,你的軟體版本號和官方的伺服器版本不一致的話就告訴你有新的功能更新了,主要用於提示使用者進行更新。序列化也一樣,我們的物件通常需要根據業務的需求變化要新增、修改或者刪除一些屬性,在我們做了一些修改後,就通過修改版本號告訴 反序列化的那一方物件有了修改你需要同步修改。

使用JDK生成的版本號和我們自定義的版本號的區別?

JDK工具生成的serialVersionUID 是根據物件的屬性資訊生成的一個編號,這就意味著只要物件的屬性有一點變動那麼他的序列化版本號就會同步進行改變。

這種情況有時候就不太友好,就像我們的軟體一樣,使用JDK生成的serialVersionUID,只要物件有一丁點改變serialVersionUID就會隨著變更,這樣的話使用者就得強制更新軟體的版本,使用者不更新就使用不了軟體。

而大多數友好的情況也許是這樣的,使用者可以選擇不更新,不更新的話使用者只是無法體驗新加的功能而已。

而這種方式就需要我們自定義的版本號了,這樣我就可以在新增了屬性後不修改serialVersionUID,反序列化的時候只是無法或許新加的屬性,並不影響程式執行。

下面用程式碼測試一下我們的理論:

(1)物件屬性序列化版本號不同進行序列化和反序列化

繼上面的例子

序列化之前我們設定serialVersionUID=2

public class User implements Serializable {

 private  static  final  long serialVersionUID=2;
 //年齡
 private int age;
 //名字
 private String name;

}

序列化儲存User到temp.txt

FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);

User user = new User();
user.setName("sandy");
user.setAge(18);
oos.writeObject(user);

oos.flush();
oos.close();

然後我們反序列化的時候物件的版本號是serialVersionUID=1

public class User implements Serializable {

 private  static  final  long serialVersionUID=1;
 //年齡
 private int age;
 //名字
 private String name;

}

最後再把從檔案資料反序列化取出來

FileInputStream fis = new FileInputStream("D:\\temp.txt");

ObjectInputStream oin = new ObjectInputStream(fis);

User user1 = (User) oin.readObject();

System.out.println("name="+user1.getName());

最後執行結果反序列化異常,原因是物件序列化和反序列化的版本號不同導致

(2)物件新增屬性,但是版本號相同也可以反序列化成功

序列化的物件資訊 這裡比反序列化的物件多了個SEX屬性

public class User implements Serializable {

 private  static  final  long serialVersionUID=1;
 //年齡
 private int age;
 //名字
 private String name;
 //年齡
 private  int sex;
}

序列化儲存User到temp.txt

FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);

User user = new User();
user.setName("sandy");
user.setAge(18);
user.setSex("女");
oos.writeObject(user);

oos.flush();
oos.close();

反序列化的物件資訊

序列化的物件資訊 這裡比序列化的物件少了個SEX屬性,但版本號一致

public class User implements Serializable {

 private  static  final  long serialVersionUID=1;
 //年齡
 private int age;
 //名字
 private String name;
 }

最後再把從檔案資料反序列化取出來

FileInputStream fis = new FileInputStream("D:\\temp.txt");

ObjectInputStream oin = new ObjectInputStream(fis);

User user1 = (User) oin.readObject();

System.out.println("name="+user1.getName());

最後控制檯列印結果正常

結果證明,只要序列化版本一樣,物件新增屬性並不會影響反序列化物件。

(3)物件新增屬性,但是版本號是使用的JDK生成序列化版本號

省略程式碼,最後執行結果報錯,原因是序列化和反序列化的版本號不一致造成。

  • 問題四:父類、子類序列化問題

序列化是以正向遞迴的形式進行的,如果父類實現了序列化那麼其子類都將被序列化;子類實現了序列化而父類沒實現序列化,那麼只有子類的屬性會進行序列化,而父類的屬性是不會進行序列化的。

(1)父類沒有實現序列化,子類實現序列化

父類

public class Parent {
 //愛好
 private String like;
}

子類

public class User extends Parent implements Serializable {

 //年齡
 private int age;
 //名字
 private String name;

}

序列化後再反序列化

//序列化User物件儲存到temp.txt
FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);

User user = new User();
user.setName("sandy");
user.setAge(18);
user.setLike("看美女");
oos.writeObject(user);

oos.flush();
oos.close();

//從temp.txt 反序列化轉為User物件
FileInputStream fis = new FileInputStream("D:\\temp.txt");

ObjectInputStream oin = new ObjectInputStream(fis);

User user1 = (User) oin.readObject();

System.out.println("like="+user1.getLike());

最後執行結果,父類屬性未被序列化

(2)父類實現序列化,子類不實現序列化

父類

public class Parent implements Serializable{
 //愛好
 private String like;


}

子類

public class User extends Parent {
 //年齡
 private int age;
 //名字
 private String name;


}

序列化後再反序列化

//序列化User物件儲存到temp.txt
FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);

User user = new User();
user.setName("sandy");
user.setAge(18);
user.setLike("看美女");
oos.writeObject(user);

oos.flush();
oos.close();

//從temp.txt 反序列化轉為User物件
FileInputStream fis = new FileInputStream("D:\\temp.txt");

ObjectInputStream oin = new ObjectInputStream(fis);

User user1 = (User) oin.readObject();

System.out.println("name="+user1.getName());

最後執行結果,子類屬性序列化正常

參考資料

https://blog.csdn.net/frankarmstrong/article/details/54959727

http://developer.51cto.com/art/200908/147650.htm
知乎連結