在Java中序列化的實現:將須要被序列化的類實現Serializable接口,該接口沒有須要實現的方法,實現該接口只是爲了標註該對象是可被序列化的,而後使用一個輸出流(如:FileOutputStream)來構造一個ObjectOutputStream(對象輸出流)對象,接着,使用ObjectOutputStream對象的writeObject(Object obj)方法就能夠將參數爲obj的對象寫出(即保存其狀態),要恢復的話則用ObjectInputStream(對象輸入流)。java
以下爲序列化、反序列化簡單案例Test01
:數組
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class Test01 { public static void main(String[] args) { //序列化操做 serializable(); //反序列化操做 deserialization(); } private static void serializable() { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) { Person person = new Person(); person.setName("張三"); person.setAge(20); oos.writeObject(person); } catch (IOException e) { e.printStackTrace(); } } private static void deserialization() { try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) { Person person = (Person) ois.readObject(); System.out.println(person); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } } //目標類實現Serializable接口 class Person implements Serializable { private static final long serialVersionUID = -2052381772192998351L; private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
上面案例中只是簡單的進行了對象序列化和反序列化,可是序列化和反序列化過程當中有不少值得思考的細節問題,例如:安全
一、序列化版本號(serialVersionUID)問題
二、靜態變量序列化
三、父類的序列化與transient
關鍵字
四、自定義序列化規則
五、序列化存儲規則服務器
一、序列化版本號(serialVersionUID)問題網絡
在寫Java程序中有時咱們常常會看到類中會有一個序列化版本號:serialVersionUID。這個值有的類是1L或者是自動生成的。ide
private static final long serialVersionUID = 1L;
或者函數
private static final long serialVersionUID = -2052381772192998351L;
當在反序列化時JVM須要判斷須要轉化的兩個類是否是同一個類,因而就須要一個序列化版本號。若是在反序列化的時候兩個類的serialVersionUID不同則JVM會拋出java.io.InvalidClassException的異常;若是serialVersionUID一致則代表能夠轉換。this
若是可序列化類未顯式聲明 serialVersionUID,則序列化運行時將基於該類的各個方面計算該類的默認 serialVersionUID 值。不過,強烈建議 全部可序列化類都顯式聲明 serialVersionUID 值,緣由是計算默認的 serialVersionUID 對類的詳細信息具備較高的敏感性,根據編譯器實現的不一樣可能千差萬別,這樣在反序列化過程當中可能會致使意外的 InvalidClassException,因此這種方式不支持反序列化重構。所謂重構就是能夠對類增長或者減小屬性字段,也就是說即便兩個類並不徹底一致,他們也是能夠轉換的,只不過若是找不到對應的字段,它的值會被設爲默認值。加密
所以,爲保證 serialVersionUID 值跨不一樣 java 編譯器實現的一致性或代碼重構時,序列化類必須聲明一個明確的 serialVersionUID 值。還強烈建議使用 private 修飾符顯示聲明 serialVersionUID(若是可能),緣由是這種聲明僅應用於直接聲明類 — serialVersionUID 字段做爲繼承成員沒有用處。數組類不能聲明一個明確的 serialVersionUID,所以它們老是具備默認的計算值,可是數組類沒有匹配 serialVersionUID 值的要求。idea
還有一個常見的值是1L(或者其餘固定值),若是全部類都這麼寫那還怎麼區分它們,這個字段還有什麼意義嗎?有的!首先若是兩個類有了相同的反序列化版本號,好比1L,那麼代表這兩個類是支持在反序列化時重構的。可是會有一個明顯的問題:若是兩個類是徹底不一樣的,可是他們的序列化版本號都是1L,那麼對於JVM來講他們也是能夠進行反序列化重構的!這這顯然是不對的,可是回過頭來講這種明顯的,愚蠢的錯誤在實際開發中是不太可能會犯的,若是不是那麼嚴謹的話用1L是個不錯的選擇。
通常的狀況下這個值是顯式地指定爲一個64位的哈希字段,好比你寫了一個類實現了java.io.Serializable接口,在idea裏會提示你加上這個序列化id。這樣作能夠區分不一樣的類,也支持反序列化重構。
總結以下:
serialVersionUID | 區分不一樣類 | 支持相同類的重構 |
---|---|---|
不指定 | YES | NO |
1L | NO | YES |
64位哈希值 | YES | YES |
簡單而言,從嚴謹性的角度來講,指定64位哈希值>默認值1L>不指定serialVersionUID值,具體怎麼使用就看你的需求啦。
二、靜態變量序列化
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class Test02 { public static void main(String[] args) { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt")); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) { //初始時avgAge爲77 Person person = new Person(); person.setName("張三"); person.setAge(20); oos.writeObject(person); //序列化後修改avgAge爲80 Person.avgAge = 80; Person person1 = (Person) ois.readObject(); //再讀取,經過person1.avgAge輸出新的值,經過實例對象訪問靜態變量原本就很反常 System.out.println(person1.avgAge); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } } //目標對象實現Serializable接口 class Person implements Serializable { private static final long serialVersionUID = -2052381772192998351L; private String name; private int age; public static int avgAge = 77; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
執行結果顯示以下:
咱們看到Test02.java
將對象序列化後,修改靜態變量的數值再將序列化對象讀取出來,而後經過讀取出來的對象得到靜態變量的數值並打印出來,最後的輸出是 10,之因此打印 10 的緣由在於序列化時,並不保存靜態變量,這其實比較容易理解,序列化保存的是對象的狀態,靜態變量屬於類的狀態,所以 序列化並不保存靜態變量 。
三、父類的序列化與transient
關鍵字
情境 :一個子類實現了 Serializable 接口,它的父類都沒有實現 Serializable 接口,序列化該子類對象,而後反序列化後輸出父類定義的某變量的數值,該變量數值與序列化時的數值不一樣。
解決 : 要想將父類對象也序列化,就須要讓父類也實現 Serializable 接口 。若是父類不實現的話的,就須要有默認的無參的構造函數 。在父類沒有實現 Serializable 接口時,虛擬機是不會序列化父對象的,而一個 Java 對象的構造必須先有父對象,纔有子對象,反序列化也不例外。因此反序列化時,爲了構造父對象,只能調用父類的無參構造函數做爲默認的父對象。所以當咱們取父對象的變量值時,它的值是調用父類無參構造函數後的值。若是你考慮到這種序列化的狀況,在父類無參構造函數中對變量進行初始化,不然的話,父類變量值都是默認聲明的值,如 int 型的默認是 0,string 型的默認是 null。
transient 關鍵字的做用是控制變量的序列化,在變量聲明前加上該關鍵字,能夠阻止該變量被序列化到文件中,在被反序列化後,transient 變量的值被設爲初始值,如 int 型的是 0,對象型的是 null。
3-一、特性使用案例:
咱們熟悉使用 transient 關鍵字可使得字段不被序列化,那麼還有別的方法嗎?根據父類對象序列化的規則,咱們能夠將不須要被序列化的字段抽取出來放到父類中,子類實現 Serializable 接口,父類不實現,根據父類序列化規則,父類的字段數據將不被序列化,造成類圖以下圖所示。
上圖中能夠看出,attr一、attr二、attr三、attr5 都不會被序列化,放在父類中的好處在於當有另一個 Child 類時,attr一、attr二、attr3 依然不會被序列化,不用重複書寫 transient 關鍵字,代碼簡潔。
四、自定義序列化規則
在序列化和反序列化過程當中須要特殊處理的類必須使用下列準確簽名來實現特殊方法:
private void writeObject(java.io.ObjectOutputStream oos) throws IOException; private void readObject(java.io.ObjectInputStream oin) throws IOException, ClassNotFoundException; private void readObjectNoData() throws ObjectStreamException;
writeObject 方法負責寫入特定類的對象的狀態,以便相應的 readObject 方法能夠恢復它。經過調用 oos.defaultWriteObject 能夠調用保存 Object 的字段的默認機制。該方法自己不須要涉及屬於其超類或子類的狀態。經過使用 writeObject 方法或使用 DataOutput 支持的用於基本數據類型的方法將各個字段寫入 ObjectOutputStream,狀態能夠被保存。
readObject 方法負責從流中讀取並恢復類字段。它能夠調用 oin.defaultReadObject 來調用默認機制,以恢復對象的非靜態和非瞬態(非 transient 修飾)字段。defaultReadObject方法使用流來分配保存在流中的對象的字段當前對象中相應命名的字段。這用於處理類演化後須要添加新字段的情形。該方法自己不須要涉及屬於其超類或子類的狀態。經過使用 writeObject 方法或使用 DataOutput 支持的用於基本數據類型的方法將各個字段寫入 ObjectOutputStream,狀態能夠被保存。
在序列化流不列出給定類做爲將被反序列化對象的超類的狀況下,readObjectNoData 方法負責初始化特定類的對象狀態。這在接收方使用的反序列化實例類的版本不一樣於發送方,而且接收者版本擴展的類不是發送者版本擴展的類時發生。在序列化流已經被篡改時也將發生;所以,無論源流是「敵意的」仍是不完整的,readObjectNoData 方法均可以用來正確地初始化反序列化的對象。
readObjectNoData()應用示例:
import java.io.FileOutputStream; import java.io.ObjectOutputStream; import java.io.Serializable; //先對舊的類對象進行序列化 public class Test03Old { public static void main(String[] args) { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) { Person person = new Person(); person.setAge(20); oos.writeObject(person); } catch (Exception e) { e.printStackTrace(); } } } class Person implements Serializable { private int age; public void setAge(int age) { this.age = age; } public int getAge() { return this.age; } }
import java.io.FileInputStream; import java.io.ObjectInputStream; import java.io.Serializable; //用新的類規範來反序列化 public class Test03New { public static void main(String[] args) { try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) { Person person = (Person) ois.readObject(); System.out.println(person.getName()); } catch (Exception e) { e.printStackTrace(); } } } //新的類繼承了Animal,這是已經序列化的舊對象裏面所沒有的內容, //因此實現readObjectNoData,能夠彌補這種因臨時擴展而沒法兼容反序列化的缺陷 class Person extends Animal implements Serializable { private int age; public void setAge(int age) { this.age = age; } public int getAge() { return this.age; } } class Animal implements Serializable { private String name; public void setName(String name) { this.name = name; } public String getName() { return this.name; } private void readObjectNoData() { this.name = "張三"; } }
將對象寫入流時須要指定要使用的替代對象的可序列化類,應使用準確的簽名來實現此特殊方法:
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
此 writeReplace 方法將由序列化調用,前提是若是此方法存在,並且它能夠經過被序列化對象的類中定義的一個方法訪問。所以,該方法能夠擁有私有 (private)、受保護的 (protected) 和包私有 (package-private) 訪問。子類對此方法的訪問遵循 java 訪問規則。
在從流中讀取類的一個實例時須要指定替代的類應使用的準確簽名來實現此特殊方法。
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
此 readResolve 方法遵循與 writeReplace 相同的調用規則和訪問規則。
TIP: readResolve經常使用來反序列單例類,保證單例類的惟一性
例如:
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class Test04Old { public static void main(String[] args) throws IOException, ClassNotFoundException { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) { oos.writeObject(Brand.NIKE); } try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) { Brand b = (Brand) ois.readObject(); // 答案顯然是false System.out.println(b == Brand.NIKE); } } } class Brand implements Serializable { private int val; private Brand(int val) { this.val = val; } // 兩個枚舉值 public static final Brand NIKE = new Brand(0); public static final Brand ADDIDAS = new Brand(1); }
答案很顯然是false,由於Brand.NIKE是程序中建立的對象,而b是從磁盤中讀取並恢復過來的對象,二者明顯來源不一樣,所以必然內存空間是不一樣的,引用(地址)顯然也是不一樣的;
但這不是咱們想看到的,由於咱們把Brand設計成枚舉類型,無論是程序中建立的仍是從哪裏讀取的,其必須應該和枚舉常量徹底相等,這纔是枚舉的意義啊!
而此時readResolve就派上用場了,咱們能夠這樣實現readResolve:
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.ObjectStreamException; import java.io.Serializable; public class Test04New { public static void main(String[] args) throws IOException, ClassNotFoundException { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) { oos.writeObject(Brand.NIKE); } try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) { Brand b = (Brand) ois.readObject(); // 答案顯然是true System.out.println(b == Brand.NIKE); } } } class Brand implements Serializable { private int val; private Brand(int val) { this.val = val; } // 兩個枚舉值 public static final Brand NIKE = new Brand(0); public static final Brand ADDIDAS = new Brand(1); private Object readResolve() throws ObjectStreamException { if (val == 0) { return NIKE; } if (val == 1) { return ADDIDAS; } return null; } }
改造之後,無論來源如何,最終獲得的都將是程序中Brand的枚舉值了!由於readResolve的代碼在執行時已經進入了程序內存環境,所以其返回的NIKE和ADDIDAS都將是Brand的靜態成員對象;
所以保護性恢復的含義就在此:首先恢復的時候沒有改變其值(val的值沒有改變)同時恢復的時候又能正常實現枚舉值的對比(地址也徹底相同);
4-一、對敏感字段加密
情境:服務器端給客戶端發送序列化對象數據,對象中有一些數據是敏感的,好比密碼字符串等,但願對該密碼字段在序列化時,進行加密,而客戶端若是擁有解密的密鑰,只有在客戶端進行反序列化時,才能夠對密碼進行讀取,這樣能夠必定程度保證序列化對象的數據安全。
解決:在序列化過程當中,虛擬機會試圖調用對象類裏的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化,該方法必需要被聲明爲private,若是沒有這樣的方法,則默認調用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用戶自定義的 writeObject 和 readObject 方法能夠容許用戶控制序列化的過程,好比能夠在序列化的過程當中動態改變序列化的數值。基於這個原理,能夠在實際應用中獲得使用,用於敏感字段的加密工做,以下代碼展現了這個過程。
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class Test05 { public static void main(String[] args) { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt")); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) { oos.writeObject(new Account()); Account account = (Account) ois.readObject(); System.out.println("解密後的字符串:" + account.getPassword()); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } } class Account implements Serializable { private String password = "123456"; public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } private void writeObject(ObjectOutputStream out) { try { ObjectOutputStream.PutField putFields = out.putFields(); System.out.println("原密碼:" + password); //模擬加密 password = "encryption"; putFields.put("password", password); System.out.println("加密後的密碼" + password); out.writeFields(); } catch (IOException e) { e.printStackTrace(); } } private void readObject(ObjectInputStream in) { try { ObjectInputStream.GetField readFields = in.readFields(); Object object = readFields.get("password", ""); System.out.println("要解密的字符串:" + object.toString()); //模擬解密,須要得到本地的密鑰 password = "123456"; } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
上述代碼中的 writeObject 方法中,對密碼進行了加密,在 readObject 中則對 password 進行解密,只有擁有密鑰的客戶端,才能夠正確的解析出密碼,確保了數據的安全。執行上述代碼後控制檯輸出以下圖所示。
4-二、序列化SDK中不可序列化的類型
4-一、對敏感字段加密
案例使用 writeObject 和 readObject 進行了對象屬性值加解密操做,有時咱們想將對象中的某一字段序列化,但它在SDK中的定義倒是不可序列化的類型,這樣的話咱們也必須把他標註爲 transient 才能保證正常序列化,但是不能序列化又怎麼恢復呢?這就用到了上面提到的 writeObject 和 readObject 方法,進行自定義序列化操做了。
示例:java.awt.geom包中的Point2D.Double類就是不可序列化的,由於該類沒有實現Serializable接口
import java.awt.geom.Point2D; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class Test06 { public static void main(String[] args) { LabeledPoint label = new LabeledPoint("Book", 5.00, 5.00); try { // 寫入前 System.out.println(label); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("object.txt")); //經過對象輸出流,將label寫入流中 out.writeObject(label); out.close(); // 寫入後 System.out.println(label); ObjectInputStream in = new ObjectInputStream(new FileInputStream("object.txt")); LabeledPoint label1 = (LabeledPoint) in.readObject(); in.close(); // 讀出並加1.0後 System.out.println(label1); } catch (Exception e) { e.printStackTrace(); } } } class LabeledPoint implements Serializable { private String label; //由於不可被序列化,因此須要加transient關鍵字 transient private Point2D.Double point; public LabeledPoint(String str, double x, double y) { label = str; //此類Point2D.Double不可被序列化 point = new Point2D.Double(x, y); } //由於Point2D.Double不可被序列化,因此須要實現下面兩個方法 private void writeObject(ObjectOutputStream oos) throws IOException { oos.defaultWriteObject(); oos.writeDouble(point.getX()); oos.writeDouble(point.getY()); } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); double x = ois.readDouble() + 1.0; double y = ois.readDouble() + 1.0; point = new Point2D.Double(x, y); } @Override public String toString() { return "LabeledPoint{" + "label='" + label + '\'' + ", point=" + point + '}'; } }
執行結果如圖所示:
在4-一、序列化SDK中不可序列化的類型
案例中,你會發現調用了defaultWriteObject()和defaultReadObject()。它們作的是默認的序列化進程,就像寫/讀全部的non-transient和 non-static字段(但他們不會去作serialVersionUID的檢查)。一般說來,全部咱們想要本身處理的字段都應該聲明爲transient。這樣的話 defaultWriteObject/defaultReadObject 即可以專一於其他字段,而咱們則可爲這些特定的字段(指transient)定製序列化。使用那兩個默認的方法並非強制的,而是給予了處理複雜應用時更多的靈活性。
五、序列化存儲規則
5-一、存儲兩次相同對象
import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class Test07 { public static void main(String[] args) { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt")); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) { //試圖將對象兩次寫入文件 Account account = new Account(); account.setPassword("123456"); oos.writeObject(account); oos.flush(); System.out.println(new File("object.txt").length()); oos.writeObject(account); System.out.println(new File("object.txt").length()); //從文件依次讀出兩個對象 Account account1 = (Account) ois.readObject(); Account account2 = (Account) ois.readObject(); //判斷兩個引用是否指向同一個對象 System.out.println(account1 == account2); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } } class Account implements Serializable { private String password; public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
上述代碼中對同一對象兩次寫入文件,打印出寫入一次對象後的存儲大小和寫入兩次後的存儲大小,而後從文件中反序列化出兩個對象,比較這兩個對象是否爲同一對象。通常的思惟是,兩次寫入對象,文件大小會變爲兩倍的大小,反序列化時,因爲從文件讀取,生成了兩個對象,判斷相等時應該是輸入 false 纔對,可是最後結果輸出如圖下圖所示。
咱們看到,第二次寫入對象時文件只增長了 5 字節,而且兩個對象是相等的,由於Java 序列化機制爲了節省磁盤空間,具備特定的存儲規則,當寫入文件的爲同一對象時,並不會再將對象的內容進行存儲,而只是再次存儲一份引用,上面增長的 5 字節的存儲空間就是新增引用和一些控制信息的空間。反序列化時,恢復引用關係,使得上述代碼中的 account1 和 account2 指向惟一的對象,兩者相等,輸出 true。該存儲規則極大的節省了存儲空間
5-二、存儲兩次相同對象,更改屬性值
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class Test08 { public static void main(String[] args) { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt")); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) { Account account = new Account(); account.setPassword("123456"); oos.writeObject(account); oos.flush(); account.setPassword("456789"); oos.writeObject(account); //從文件依次讀出兩個對象 Account account1 = (Account) ois.readObject(); Account account2 = (Account) ois.readObject(); System.out.println(account1.getPassword()); System.out.println(account2.getPassword()); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } } class Account implements Serializable { private String password; public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
執行結果以下圖:
上述代碼的目的是但願將 account 對象兩次保存到 object.txt 文件中,寫入一次之後修改對象屬性值再次保存第二次,而後從 object.txt 中再依次讀出兩個對象,輸出這兩個對象的 password 屬性值。上述代碼的目的本來是但願一次性傳輸對象修改先後的狀態。
結果兩個輸出的都是 123456, 緣由就是第一次寫入對象之後,第二次再試圖寫的時候,虛擬機根據引用關係知道已經有一個相同對象已經寫入文件,所以只保存第二次寫的引用,因此讀取時,都是第一次保存的對象。這也驗證了5-一、存儲兩次相同對象
案例的現象,相同對象存在只會存儲引用,再也不進行對象存儲,因此第二次修改的屬性未變化。讀者在使用一個文件屢次 writeObject 須要特別注意這個問題。