序列化是指對象經過寫出描述本身狀態的數值來記錄本身的過程,即將對象表示成一系列有序字節,Java提供了將對象寫入流和從流中恢復對象的方法。對象能包含其它的對象,而其它的對象又能夠包含另外的對象。Java序列化可以自動的處理嵌套的對象。對於一個對象的簡單域,writeObject()直接將其值寫入流中。當遇到一個對象域時,writeObject()被再次調用,若是這個對象內嵌另外一個對象,那麼,writeObject()又被調用,直到對象能被直接寫入流爲止。程序員所須要作的是將對象傳入ObjectOutputStream的writeObject()方法,剩下的將有系統自動完成。
要實現序列化的類必須實現的java.io.Serializable或java.io.Externalizable接口,不然將產生一個NotSerializableException。該接口內部並無任何方法,它只是一個"tagging interface",僅僅"tags"它本身的對象是一個特殊的類型。類經過實現 java.io.Serializable接口以啓用其序列化功能。未實現此接口的類將沒法使其任何狀態序列化或反序列化。可序列化類的全部子類型自己都是可序列化的。序列化接口沒有方法或字段,僅用於標識可序列化的語義。Java的"對象序列化"能讓你將一個實現了Serializable接口的對象轉換成一組byte,這樣往後要用這個對象時候,你就能把這些byte數據恢復出來,並據此從新構建那個對象了。java
Java中,一切都是對象,在分佈式環境中常常須要將Object從這一端網絡或設備傳遞到另外一端。這就須要有一種能夠在兩端傳輸數據的協議。Java序列化機制就是爲了解決這個問題而產生。程序員
Java序列化支持的兩種主要特性:bash
Java序列化的目的(我目前能理解的):
網絡
下面咱們經過一個簡單的例子來看下Java默認支持的序列化。咱們先定義一個類,而後將其序列化到文件中,最後讀取文件從新構建出這個對象。在序列化一個對象的時候,有幾點須要注意下:
分佈式
class SuperClass implements Serializable{
private String name;
private int age;
private String email;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getEmail() {
return email;
}
public SuperClass(String name,int age,String email) {
this.name=name;
this.age=age;
this.email=email;
}
}
複製代碼
下面咱們來看下main方法裏面的序列化過程,代碼以下:ide
public static void main(String[] args) throws IOException,ClassNotFoundException {
System.out.println("序列化對象開始!");
SuperClass superClass=new SuperClass("gong",27, "1301334028@qq.com");
File rootfile=new File("C:/data");
if(!rootfile.exists()) {
rootfile.mkdirs();
}
File file=new File("C:/data/data.txt");
if(!file.exists()) {
file.createNewFile();
}
FileOutputStream fileOutputStream=new FileOutputStream(file);
ObjectOutputStream objectOutputStream=new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(superClass);
objectOutputStream.flush();
objectOutputStream.close();
System.out.println("序列化對象完成!");
System.out.println("反序列化對象開始!");
FileInputStream fileInputStream=new FileInputStream(new File("C:\\data\\data.txt"));
ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
SuperClass getObject=(SuperClass) objectInputStream.readObject();
System.out.println("反序列化對象數據:");
System.out.println("name:"+getObject.getName()+"\nage:"+getObject.getAge()+"\nemail:"+getObject.getEmail());
}
複製代碼
代碼運行結果以下:函數
序列化對象開始!
序列化對象完成!
反序列化對象開始!
反序列化對象數據:
name:gong
age:27
email:1301334028@qq.com
複製代碼
經過上面的例子,咱們看到Java默認提供了序列化與反序列化機制,對於單個實體類來講,整個過程都是自動完成的,無需程序員進行額外的干預。若是咱們想讓某些關鍵的域不參與序列化過程呢?Java提供了方法,接着往下看。工具
若是咱們如今想讓上面SuperClass類走age和email不參與序列化過程,那麼只須要在其定義前面加上transient關鍵字便可:ui
private transient int age;
private transient String email;
複製代碼
這樣咱們在進行序列化的時候,字節流中不不包含age和email的數據的,反序列的時候會賦予這兩個變量默認值。仍是運行剛纔的工程,這時候咱們結果以下:this
序列化對象開始!
序列化對象完成!
反序列化對象開始!
反序列化對象數據:
name:gong
age:0
email:null
複製代碼
若是默認的序列化過程不能知足需求,咱們也能夠自定義整個序列化過程。這時候咱們只須要在須要序列化的類中定義writeObject方法和readObject方法便可。咱們仍是以SuperClass爲例,如今咱們添加自定義的序列化過程,transient關鍵字讓Java內置的序列化過程忽略修飾的變量,咱們經過自定義序列化過程,仍是序列化age和email,咱們來看看改動後的結果:
private String name;
private transient int age;
private transient String email;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getEmail() {
return email;
}
public SuperClass(String name,int age,String email) {
this.name=name;
this.age=age;
this.email=email;
}
private void writeObject(ObjectOutputStream objectOutputStream)
throws IOException {
objectOutputStream.defaultWriteObject();
objectOutputStream.writeInt(age);
objectOutputStream.writeObject(email);
}
private void readObject(ObjectInputStream objectInputStream)
throws ClassNotFoundException,IOException {
objectInputStream.defaultReadObject();
age=objectInputStream.readInt();
email=(String)objectInputStream.readObject();
}
複製代碼
運行結果以下:
反序列化對象數據:
name:gong
age:27
email:1301334028@qq.com
複製代碼
咱們看到,執行結果和默認的結果是一致的,咱們經過自定義序列化機制,修改了默認的序列化過程(讓transient關鍵字失去了做用)。
注意:
細心的同窗可能發現了咱們在自定義序列化的過程當中調用了defaultWriteObject()和defaultReadObject()方法。這兩個方法是默認的序列化過程調用的方法。若是咱們自定義序列化過程僅僅調用了這兩個方法而沒有任何額外的操做,這其實和默認的序列化過程沒任何區別,你們能夠試一下。
默認狀況下是這樣的
子類實現了Serializable接口,父類沒有,父類中的屬性不能序列化(不報錯,數據丟失),可是在子類中屬性仍能正確序列化。
若是咱們想在序列化的時候保存父類的域,那麼在序列化子類實例的時候必須顯式的保存父類的狀態。咱們將前面的例子稍做修改:
class SuperClass{
protected String name;
protected int age;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public SuperClass(String name,int age) {
this.name=name;
this.age=age;
}
}
class DeriveClass extends SuperClass implements Serializable{
private String email;
private String address;
public DeriveClass(String name,int age,String email,String address) {
super(name,age);
this.email=email;
this.address=address;
}
public String getEmail() {
return email;
}
public String getAddress() {
return address;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject(name);
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
name=(String)in.readObject();
age=in.readInt();
}
@Override
public String toString() {
return "name:"+getName()+"\nage:"+getAge()+"\nemail:"+getEmail()+"\naddress"+getAddress();
}
}
複製代碼
main方法咱們修改成序列化子類對象便可:
DeriveClass superClass=new DeriveClass("gong",27,"1301334028@qq.com","NJ");
DeriveClass getObject=(DeriveClass) objectInputStream.readObject();
System.out.println("反序列化對象數據:");
System.out.println(getObject);
複製代碼
運行代碼發現報錯了,報錯以下:
Exception in thread "main" java.io.InvalidClassException: com.learn.example.DeriveClass; no valid constructor
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(Unknown Source)
at java.io.ObjectStreamClass.checkDeserialize(Unknown Source)
at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)
at com.learn.example.RunMain.main(RunMain.java:88)
複製代碼
咱們來仔細分析下,爲何會這樣。DeriveClass支持序列化,其父類不支持序列化,因此這種狀況下,子類在序列化的時候須要額外的序列化父類的域(若是有這個須要的話)。那麼在反序列的時候,因爲構建DeriveClass實例的時候須要先調用父類的構造函數,而後纔是本身的構造函數。反序列化時,爲了構造父對象,只能調用父類的無參構造函數做爲默認的父對象,所以當咱們取父對象的變量值時,它的值是調用父類無參構造函數後的值。若是你考慮到這種序列化的狀況,在父類無參構造函數中對變量進行初始化。或者在readObject方法中進行賦值。 咱們只須要在SuperClass中添加一個空的構造函數便可:
public SuperClass() {}
複製代碼
這種狀況下,子類也支持序列化操做的。通常狀況下,無需作特殊的操做便可。
上面的例子,咱們都沒有看到這個serialVersionUID這個字段,爲何咱們也能正常的序列化也反序列化呢?這是由於Eclipse默認爲咱們生成了一個序列化ID。
Eclipse下提供了兩種生成策略,一個是固定的1L,一個是隨機生成一個不重複的long類型數據(其實是使用JDK工具生成),在這裏有一個建議,若是沒有特殊需求,就是用默認的1L就能夠,這樣能夠確保代碼一致時反序列化成功。
注意:虛擬機是否容許反序列化,不只取決於類路徑和功能代碼是否一致,一個很是重要的一點是兩個類的序列化ID是否一致(就是 privatestatic final long serialVersionUID = 1L)雖然兩個類的功能代碼徹底一致,可是序列化ID不一樣,他們沒法相互序列化和反序列化(這種狀況特別是在網絡傳輸後,遠程創建對象的時候須要注意)
經過前面的例子,咱們將數據序列化到data.txt文件中,下面咱們經過二進制查看工具來看下Java序列化後的字節流是如何存儲到文件中的,它的格式是怎麼樣的?咱們將上面的SuperClass類改造下:
class SuperClass implements Serializable{
private static final int serialVersionUID=1;
protected String name;
protected int age;
public SuperClass() {}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public SuperClass(String name,int age) {
this.name=name;
this.age=age;
}
@Override
public String toString() {
return "name:"+getName()+"\nage:"+getAge();
}
}
複製代碼
寫入的數據以下:
SuperClass superClass=new SuperClass("gong",27);
複製代碼
下面咱們打開data.txt來看下存儲的內容:具體的存儲內容如圖所示: