深刻理解Java序列化機制

一、Java序列化介紹

序列化是指對象經過寫出描述本身狀態的數值來記錄本身的過程,即將對象表示成一系列有序字節,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 的RMI使原本存在於其餘機器的對象能夠表現出就象本地機器上的行爲。
  • 將消息發給遠程對象時,須要經過對象序列化來傳輸參數和返回值

Java序列化的目的(我目前能理解的):
網絡

  • 支持運行在不一樣虛擬機上不一樣版本類之間的雙向通信;
  • 提供對持久性和RMI的序列化;

三、關於序列化的一些例子

下面咱們經過一個簡單的例子來看下Java默認支持的序列化。咱們先定義一個類,而後將其序列化到文件中,最後讀取文件從新構建出這個對象。在序列化一個對象的時候,有幾點須要注意下:
分佈式

  • 當一個對象被序列化時,只序列化對象的非靜態成員變量,不能序列化任何成員方法和靜態成員變量。
  • 若是一個對象的成員變量是一個對象,那麼這個對象的數據成員也會被保存。
  • 若是一個可序列化的對象包含對某個不可序列化的對象的引用,那麼整個序列化操做將會失敗,而且會拋出一個NotSerializableException。能夠經過將這個引用標記爲transient,那麼對象仍然能夠序列化。對於一些比較敏感的不想序列化的數據,也能夠採用該標識進行修飾。
    下面咱們先經過一個簡單的例子來看一下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提供了方法,接着往下看。工具

transient關鍵字與序列化

若是咱們如今想讓上面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

上面的例子,咱們都沒有看到這個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來看下存儲的內容:具體的存儲內容如圖所示:

下面咱們就來詳細解釋每一步的內容。

第1部分是序列化文件頭

  • AC ED:STREAM_MAGIC序列化協議
  • 00 05:STREAM_VERSION序列化協議版本
  • 73:TC_OBJECT聲明這是一個新的對象

第2部分是要序列化的類的描述,在這裏是SerializableObject類

  • 72:TC_CLASSDESC聲明這裏開始一個新的class
  • 00 1C:十進制的28,表示class名字的長度是28個字節
  • 63 6F 6D ... 61 73 73:表示的是「com.learn.example.SuperClass」這一串字符,能夠數一下確實是28個字節
  • 00 00 00 00 00 00 00 01:SerialVersion,咱們在這個類裏面設置的值是1,若是咱們不設置的話,Eclipse會爲咱們自動設置一個。
  • 02:標記號,聲明該對象支持序列化
  • 00 02:該類所包含的域的個數爲2個

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

  • 4C:字符"L",表示該屬性是一個對象類型而不是一個基本類型
  • 00 03十進制的3,表示屬性名的長度
  • 61 67 65:字符串「age」,屬性名
  • 4C:字符"L",表示該屬性是一個對象類型而不是一個基本類型
  • 00 04十進制的4,表示屬性名的長度
  • 6E 61 6D 65:字符串「name」,屬性名
  • 74:TC_STRING,表明一個new String,用String來引用對象

第4部分是該對象父類的信息,若是沒有父類就沒有這部分。有父類和第2部分差很少

  • 00 12:十進制的18,表示父類的長度
  • 4C 6A 61 ... 6E 67 3B:「L/java/lang/String;」表示的是父類屬性
  • 78:TC_ENDBLOCKDATA,對象塊結束的標誌
  • 70:TC_NULL,說明沒有其餘超類的標誌

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

  • 00 00 00 1B:屬性值 age=27
  • 74:TC_STRING,表明一個new String,用String來引用對象
  • 00 04十進制的4,表示屬性名的長度
  • 67 6F 6E 67 name屬性的值gong
    從以上對於序列化後的二進制文件的解析,咱們能夠得出如下幾個關鍵的結論:
  • 一、序列化以後保存的是對象的信息
  • 二、被聲明爲transient的屬性不會被序列化,這就是transient關鍵字的做用
  • 三、被聲明爲static的屬性不會被序列化,這個問題能夠這麼理解,序列化保存的是對象的狀態,可是static修飾的變量是屬於類的而不是屬於對象的,所以序列化的時候不會序列化它
相關文章
相關標籤/搜索