一文完全搞懂JAVA序列化

前言

如今開發過程當中常常遇到多個進程多個服務間須要交互,或者不一樣語言的服務之間須要交互,這個時候,咱們通常選擇使用固定的協議,將數據傳輸過去,可是在不少語言,好比java等jvm語言中,傳輸的數據是特有的類對象,而類對象僅僅在當前jvm是有效的,傳遞給別的jvm或者傳遞給別的語言的時候,是沒法直接識別類對象的,那麼,咱們須要多個服務之間交互或者不一樣語言交互,該怎麼辦?這個時候咱們就須要經過固定的協議,傳輸固定的數據格式,而這個數據傳輸的協議稱之爲序列化,而定義了傳輸數據行爲的框架組件也稱之爲序列化組件(框架)java

序列化有什麼意義

首先咱們先看看,java中的序列化,在java語言中實例對象想要序列化傳輸,須要實現Serializable 接口,只有當前接口修飾定義的類對象才能夠按照指定的方式傳輸對象。而傳輸的過程當中,須要使用java.io.ObjectOutputStream 和java.io.ObjectInputStream 來實現對象的序列化和數據寫入,接着咱們看一個最基礎的序列化:markdown

咱們建立一個java實體類:框架

public class User {
	private Integer id;
	private String name;
	private Byte sex;
	private Integer age;
	public Integer getId() {
		return id;
	}
	public void setId(Integer id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Byte getSex() {
		return sex;
	}
	public void setSex(Byte sex) {
		this.sex = sex;
	}
	public Integer getAge() {
		return age;
	}
	public void setAge(Integer age) {
		this.age = age;
	}
    @Override
	public String toString() {
		return "User [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + "]";
	}
}
複製代碼

而後咱們編寫發送對象(序列化)的實現:jvm

public class OutPutMain {
   public static void main( String[] args ) throws UnknownHostException, IOException {
       Socket socket = new Socket("localhost",8080);
       try(ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream())){
       	User user = new User().setAge(10).setId(10).setName("張三").setSex((byte)0);
       	outputStream.writeObject(user);
       	outputStream.flush();
       	System.out.println("對象已經發送:--->"+user);
       }catch (Exception e) {
       	e.getStackTrace();
       	System.err.println("對象發送失敗:--->");
   	}finally{
   		if(!socket.isClosed()){
   			socket.close();
   		}
   	}
   }
}
複製代碼

而後定義讀取實體(反序列化)的代碼:socket

public class InputMain {
	public static void main(String[] args) throws IOException {
		ServerSocket serverSocket = new ServerSocket(8080);
		Socket socket = serverSocket.accept();
		try(ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream())){
			User user = (User) inputStream.readObject();
			System.out.println(user);
		}catch (Exception e) {
			e.getStackTrace();
		}finally {
			if(!serverSocket.isClosed()){
				serverSocket.close();
			}
		}
	}
}
複製代碼

接着咱們先運行InputMain,再運行OutPutMain,看下結果:ide

java.io.NotSerializableException: demo.ser.User
	at java.io.ObjectOutputStream.writeObject0(Unknown Source)
	at java.io.ObjectOutputStream.writeObject(Unknown Source)
	at demo.ser.OutPutMain.main(OutPutMain.java:15)
複製代碼

很明顯報錯了,告訴咱們user類不能序列化,緣由很明顯,咱們的User類沒有實現Serializable接口,如今咱們修改以下:學習

public class User implements Serializable{
複製代碼

而後咱們再按照順序執行一次後,就能看到打印的結果了:測試

對象已經發送:--->User [id=10, name=張三, sex=0, age=10]
複製代碼

serialVersionUID的認知

上面咱們學習了一個最基礎的序列化傳遞的方法,可是咱們仔細觀察代碼,發現編譯器在class申明那裏報了一個黃色的波浪線,這個是爲何呢?原來jdk推薦咱們實現序列化接口後,讓咱們再去生成一個固定的序列化id--serialVerionUID,而這個id的做用是用來做爲傳輸/讀取雙端進程的版本是否一致的,防止咱們由於版本不一致致使的序列化失敗,那麼serialVerionUID取值應該如何取值?又或者serialVerionUID不一致的時候,是否是序列化會失敗呢?接下來咱們來看看serialVerionUID的取值方案:ui

能夠看到編譯器推薦咱們有兩種方式,一種是生成默認的versionID,這個值爲1L,還有一種方式是根據類名、接口名、成員方法及屬性等來生成一個 64 位的哈希字段,只要咱們類名、方法名、變量有修改,或者有空格、註釋、換行等操做,計算出來的哈希字段都會不一樣,固然這裏須要注意,每次咱們有以上的操做的時候儘可能都要從新生成一次serialVerionUID(編譯器並不會給你自動修改)。接下來咱們來看下一個問題,若是咱們修改了serialVerionUID,而另外一個的serialVerionUID仍是原來的,咱們可否序列化,是否會有影響呢?咱們把上述的案例修改下:this

OutPutMain對應的User類的serialVerionUID修改成2L:

public class User implements Serializable{
	private static final long serialVersionUID = 2L;
    ........
複製代碼

而InputMain對應的User仍是使用的默認值:

public class User implements Serializable{
	private static final long serialVersionUID = 1L;
    ........
複製代碼

再次運行一下,果不其然,拋出了InvalidClassException,告訴咱們序列化id不同,致使傳輸失敗:

java.io.InvalidClassException: demo.ser.User; local class incompatible: stream classdesc serialVersionUID = 2, 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 demo.ser.InputMain.main(InputMain.java:13)
複製代碼

serialVersionUID兩種方式的區別及選擇

那麼又有個問題出現了,既然這個serialVersionUID如此重要,那麼編譯器推薦咱們兩種方法,咱們到底該如何選擇,這兩種區別又在哪?上面咱們也知道兩種序列化UID一個是固定的1L默認值,一個是按照類方法屬性等計算出來的hash,只要有代碼的修改,從新計算出來的結果就會改變,因此兩個id一個是固定的,除非手動修改,另一個能夠認爲每次修改完都會變化(實際上是須要咱們從新生成),根據這個特性,咱們能夠分別用在不一樣的場景下,好比,咱們的一些dto與業務並沒有太大關係,很長時間甚至整個項目週期中,都是固定不會進行改變或者不多改變的dto,這裏的dto建議使用默認值方式,一樣也防止由於誤操做等方式致使uid改變形成序列化失敗(好比不當心修改了順序等,若是是第二種方式,從新生成的話,就會改變),也能夠在基礎庫或者基礎jar中定義的dto使用固定UID方式,保證dto的穩定,而在業務線開發過程當中,我習慣動態生成UID,尤爲是頻繁修改的dto中,更是須要如此,防止在開發階段一些未知的序列化問題或者未知問題沒有被檢測出來,而serialVersionUID的做用就是在序列化的時候,判斷兩個dto是否一致,也是jdk實現的接口規則,防止序列化不一致致使問題,除此以外並沒有其餘區別

Transient關鍵字

到如今咱們已經知道了序列化的大概使用方式,可是這個時候咱們遇到一個需求,一個dto在使用的時候須要有這個字段完成業務流程,可是序列化的時候咱們不須要這個字段,該如何呢?這個時候就須要Transient關鍵字了,這個是java針對序列化出的關鍵字,修飾在指定字段上,能夠在序列化的時候,排除當前關鍵字修飾的字段,僅序列化其餘字段,當咱們反序列化的時候,能夠看到基礎類型爲默認值,引用類型則爲null,代碼以下:

修改outPutMain工程的User類:

public class User implements Serializable{
	private static final long serialVersionUID = 2L;
	//不序列化id字段
	private transient Integer id;
	private String name;
	private Byte sex;
	private Integer age;
    ......
複製代碼

再次進行序列化後能夠看到序列化的結果以下:

對象已經發送:--->User [id=10, name=張三, sex=0, age=10]
複製代碼

可是反序列化的結果以下:

User [id=null, name=張三, sex=0, age=10]
複製代碼

能夠看到,當前的id字段果真沒有任何結果,可是這個時候咱們不由懷疑,若是這個dto恰好沒有id字段,其餘徹底同樣,而且故意把serialVersionUID也設置爲同樣的,咱們序列化會有問題嗎?接着咱們把IntputMain工程的User類的id字段移除,再來看下運行結果:

序列化的結果和上面同樣:

對象已經發送:--->User [id=10, name=張三, sex=0, age=10]
複製代碼

可是反序列化的結果竟然沒有出現序列化異常,並且成功的完成了反序列化操做:

User [name=張三, sex=0, age=10]
複製代碼

怎麼會這樣呢?原來transient關鍵字會把全部屬性都序列化到IO(內存、硬盤)等,可是有了當前關鍵字修飾的屬性並不會包含在序列化中,因此當序列化完成後,已經丟失了transient修飾的屬性信息,而在反序列化的時候,是按照序列化的結果來反向給屬性賦值,因此咱們反序列化的屬性存在多餘的或者僅和序列化結果一致,缺乏幾個屬性也是能夠的,因此咱們經過以上的案例咱們能夠總結如下三點:

1)一旦變量被transient修飾,變量將再也不是對象持久化的一部分,該變量內容在序列化後沒法得到訪問

2)transient關鍵字只能修飾變量,而不能修飾方法和類。注意,本地變量是不能被transient關鍵字修飾的。變量若是是用戶自定義類變量,則該類須要實現Serializable接口

3)java的序列化機制是向上兼容的,也就是說,能夠包含或者超過序列化的屬性,可是當反序列化的時候缺乏屬性,序列化就會失敗

而序列化的時候還須要注意一點,序列化不是萬能的,除了transient關鍵字外,若是某個屬性存在static關鍵字修飾,那麼不管是否有transient修飾,都不能參與序列化

可能有人會比較疑惑,若是咱們給id屬性使用static修飾,而且初始化的時候設置了值,可是序列化完成後咱們依然收到了以前設置的值,這不是和上面的描述矛盾嗎?其實否則,咱們都知道static在jvm加載的過程當中會有惟一一份初始化的結果,而咱們拿到的所謂序列化的值,是由於jvm初始化的值,而不是序列化帶來的值,接着咱們修改上面的案例來檢測下:

將兩個工程中得User類修改以下:

public class User implements Serializable{
	private static final long serialVersionUID = 1L;
	
	private Integer id;
	public static String name;
	private Byte sex;
	private Integer age;
    ........
複製代碼

而後修改反序列化(InputMain)工程的main代碼:

public static void main(String[] args) throws IOException {
		ServerSocket serverSocket = new ServerSocket(8080);
		Socket socket = serverSocket.accept();
		try(ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream())){
			//在反序列化以前設置一個值
			User.name = "李四";
			//進行反序列化
			User user = (User) inputStream.readObject();
			System.out.println(user);
		}catch (Exception e) {
			e.printStackTrace();
		}finally {
			if(!serverSocket.isClosed()){
				serverSocket.close();
			}
		}
	}
複製代碼

能夠看到這裏咱們給值修改成李四,若是結論正確,那麼結果應該爲李四而不是初始化傳遞的張三,如今咱們看下序列化的對象:

對象已經發送:--->User [id=10, name=張三, sex=0, age=10]
複製代碼

再來看反序列化的結果:

User [id=10, name=李四, sex=0, age=10]
複製代碼

果真是按照靜態加載的結果來的,而不是序列化,從而肯定結論是正確的

Externalizable 自定義序列化

若是這個時候有人會說,transient關鍵字不夠靈活啊,若是我須要動態的指定哪些能夠序列化哪些不能序列化,該怎麼辦?這個時候咱們不妨考慮Externalizable 接口,這個接口是Serializable接口的子接口,使用當前接口的時候必須存在無參構造,接口定義以下:

public interface Externalizable extends Serializable {  
    public void writeExternal(ObjectOutput  out) throws IOException ;  
    public void readExternal(ObjectInput in) throws IOException,ClassNot FoundException ;  
} 
複製代碼

能夠看到咱們實現當咱們實現當前接口的時候,必需要重寫writeExternal和readExternal兩個方法,而當前的兩個方法做用則是自定義序列化和反序列化的操做,接着咱們經過自定義的序列化實現id不被序列化的操做:

public class NUser implements Externalizable {
	private Integer id;
	private String name;
	private Byte sex;
	private Integer age;
	
	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

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

	public Byte getSex() {
		return sex;
	}

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

	public Integer getAge() {
		return age;
	}

	public void setAge(Integer age) {
		this.age = age;
	}

	//反序列化的時候調用--自定義反序列化
	@Override
	public void readExternal(ObjectInput input) throws IOException, ClassNotFoundException {
       //按照序列化的順序獲取反序列化的字段
		this.name = input.readObject().toString();
		this.sex = input.readByte();
		this.age = input.readInt();
	}

	//序列化的時候調用--自定義序列化
	@Override
	public void writeExternal(ObjectOutput output) throws IOException {
		output.writeObject(this.name);
		output.writeByte(this.sex);
		output.writeInt(this.age);
	}

	@Override
	public String toString() {
		return "NUser [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + "]";
	}
}
複製代碼

從上面能夠看出來,實現了Externalizable接口之後,編譯器不能自動實現serialVersionUID,須要咱們給OutPutMain和InputMain工程手動添加以下代碼:

private static final long serialVersionUID = 1L;
複製代碼

由於當前徹底屬於自定義的序列化,系統再也不提供默認的方式和自動計算的hash方式,而是徹底由咱們決定是否建立serialVersionUID以及對應的版本,接着將OutPutMain工程下的代碼修改:

//User user = new User().setAge(10).setId(10).setName("張三").setSex((byte)0);
NUser user = new NUser().setAge(10).setId(10).setName("張三").setSex((byte)0);
複製代碼

InputMain工程的代碼修改成:

//User user = (User) inputStream.readObject();
NUser user = (NUser) inputStream.readObject();
複製代碼

接着序列化的結果以下:

對象已經發送:--->NUser [id=10, name=張三, sex=0, age=10]
複製代碼

反序列化的結果爲:

NUser [id=null, name=張三, sex=0, age=10]
複製代碼

能夠看到徹底按照咱們的序列化方式來操做了,這樣就能夠實現靈活的序列化/反序列化代碼了

writeObject 和 readObject

經過Externalizable接口咱們能夠實現自定義的序列化和反序列化,可是咱們能夠看到這兩個操做須要依賴readExternal和writeExternal方法實現,而這兩個方法內部是依賴了ObjectInput和ObjectOutput實現的自定義,這個時候咱們不由疑問,難道序列化機制和IO流有關係?ObjectInput接口咱們知道,內部定義了不少read相關的方法,最多見的實現類爲ObjectInputStream,而ObjectOutput內部定義了不少write相關的方法,常見的實現類爲ObjectInputStream,那麼咱們能夠大膽猜想是由於writeObject和readObject方法實現的,如今咱們修改兩個工程中的NUser類以下:

public class NUser implements Serializable{
	private static final long serialVersionUID = 1L;

	private Integer id;
	private String name;
	private Byte sex;
	private Integer age;

	public Integer getId() {
		return id;
	}

	public NUser setId(Integer id) {
		this.id = id;
		return this;
	}

	public String getName() {
		return name;
	}

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

	public Byte getSex() {
		return sex;
	}

	public NUser setSex(Byte sex) {
		this.sex = sex;
		return this;
	}

	public Integer getAge() {
		return age;
	}

	public NUser setAge(Integer age) {
		this.age = age;
		return this;
	}

	private void writeObject(ObjectOutputStream output) throws IOException{
		output.writeObject(this.name);
		output.writeByte(this.sex);
		output.writeInt(this.age);
	}

	private void readObject(ObjectInputStream input) throws IOException,ClassNotFoundException{
		//按照序列化的順序獲取反序列化的字段
		this.name = input.readObject().toString();
		this.sex = input.readByte();
		this.age = input.readInt();
	}

	@Override
	public String toString() {
		return "NUser [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + "]";
	}
}
複製代碼

能夠看到咱們和以前重寫Externalizable的兩個方法同樣的寫法,再次運行序列化後,結果以下:

對象已經發送:--->NUser [id=10, name=張三, sex=0, age=10]
複製代碼
NUser [id=null, name=張三, sex=0, age=10]
複製代碼

是否是和以前的結果同樣?因此能夠看到咱們的猜想是正確的,而且咱們在查看源碼後能夠看到:

咱們的readObject/writeObjet方法 是經過反射來調用的,因此最終都是會調用了readObject/writeObject方法來實現

Java序列化使用的總結

經過上面的案例測試和比較,咱們能夠獲得序列化使用的一些經驗總結:

  1. Java 序列化只是針對對象的屬性的傳遞,至於方法和序列化過程無關
  2. 當一個父類實現了序列化,那麼子類會自動實現序列化,不須要顯示實現序列化接口,反過來,子類實現序列化,而父類沒有實現序列化則序列化會失敗---即序列化具備傳遞性
  3. 當一個對象的實例變量引用了其餘對象,序列化這個對象的時候會自動把引用的對象也進 行序列化(實現深度克隆)
  4. 當某個字段被申明爲 transient 後,默認的序列化機制會忽略這個字段
  5. 被申明爲 transient 的字段,若是須要序列化,能夠添加兩個私有方法:writeObject 和 readObject或者實現Externalizable接口
相關文章
相關標籤/搜索