秒懂Java序列化與反序列化
概述
什麼是序列化?什麼是反序列化?為什麼需要序列化?如何序列化?應該注意什麼?本文將從這幾方面來論述。
定義
什麼是序列化?什麼是反序列化?
序列化: 把Java物件轉換為位元組序列的過程。
反序列化:把位元組序列恢復為Java物件的過程。
作用
為什麼需要序列化?
在當今的網路社會,我們需要在網路上傳輸各種型別的資料,包括文字、圖片、音訊、視訊等, 而這些資料都是以二進位制序列的形式在網路上傳送的,那麼傳送方就需要將這些資料序列化為位元組流後傳輸,而接收方接到位元組流後需要反序列化為相應的資料型別。當然接收方也可以將接收到的位元組流儲存到磁碟中,等到以後想恢復的時候再恢復。
綜上,可以得出物件的序列化和反序列化主要有兩種用途:
- 把物件的位元組序列永久地儲存到磁碟上。(持久化物件)
- 可以將Java物件以位元組序列的方式在網路中傳輸。(網路傳輸物件)
如何實現
如何序列化和反序列化?
如果要讓某個物件支援序列化機制,則其類必須實現下面這兩個介面中任一個。
Serializable
public interface Serializable { }
Externalizable
public interface Externalizable extends java.io.Serializable { void writeExternal(ObjectOutput out) throws
實現Serializable介面
簡單實現
如果是對序列化的需求非常簡單,沒有對序列化過程控制的需求,可以簡單實現Serializable
介面即可。
從Serializable
的原始碼可知,其是一個標記介面,無需實現任何方法。例如我們有如下的Student
類
public class Student implements Serializable {
private String name;
private int age;
public Student(String name,int age)
{
System.out.println("有引數構造器執行");
this.name=name;
this.age=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;
}
}
序列化: 那麼我們如何將此類的物件序列化後儲存到磁碟上呢?
- 建立一個
ObjectOutputStream
輸出流oos - 呼叫此輸出流oos的
writeObject()
方法
private static void serializ()
{
try (ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("object.txt"));)
{
Student s=new Student("ben",18);
oos.writeObject(s);
} catch (IOException e) {
e.printStackTrace();
}
}
上面程式碼將Sutent 的一個例項物件序列化到了一個文字檔案中。
反序列化:我們如從文字檔案中將此物件的位元組序列恢復成Student
物件呢?
- 建立一個
ObjectInputStream
輸入流ois 呼叫此輸入流ois的
readObject()
方法。private static void deSerializ() { try(ObjectInputStream ois=new ObjectInputStream(new FileInputStream("object.txt"));) { Student s= (Student) ois.readObject(); System.out.println(s.toString()); }catch (Exception e) { e.printStackTrace(); } }
Node: 當反序列化的時候並沒有呼叫
Student
的建構函式,說明反序列化機制無需通過構造器來構建Java物件,這就給實現了序列化機制的單例模式造成了麻煩。
版本 serialVersionUID
由於反序列化Java物件的時候,必須提供該物件的class檔案,但是隨著專案的升級class檔案檔案也會升級,Java如何保證相容性呢?答案就是 serialVersionUID
。每個可以序列化的類裡面都會存在一個serialVersionUID
,只要這個值前後一致,即使類升級了,系統仍然會認為他們是同一版本。如果我們不顯式指定一個,系統就會使用預設值。
```
public class Student implements Serializable {
private static final long serialVersionUID=1L;
...
}
```
我們應該總是顯式指定一個版本號,這樣做的話我們不僅可以增強對序列化版本的控制,而且也提高了程式碼的可移植性。因為不同的JVM有可能使用不同的策略來計算這個版本號,那樣的話同一個類在不同的JVM下也會認為是不同的版本。
那麼我們如何維護這個版本號呢?
- 只修改了類的方法,無需改變
serialVersionUID
; - 只修改了類的static變數和使用transient 修飾的例項變數,無需改變
serialVersionUID
; - 如果修改了例項變數的型別,例如一個變數原來是
int
改成了String
,則反序列化會失敗,需要修改serialVersionUID
;如果刪除了類的一些例項變數,可以相容無需修改;如果給類增加了一些例項變數,可以相容無需修改,只是反序列化後這些多出來的變數的值都是預設值。
繼承及引用物件序列化
當要序列化的類存在父類的時候,直接或者間接福來,其父類也必須可以序列化。
當要序列化的類中引用了其他類的物件,那麼這些物件的類也必須是可序列化的,如下面程式碼中的Teacher
類也必須是可以序列化的
public class Student implements Serializable {
private Teacher teacher;
...
}
Java序列化演算法
Java序列化遵循以下演算法:
- 所有序列化過的,包括磁碟中的的例項物件都有一個序列化編號
- 當試圖序列化一個物件時,程式會先檢查該物件是否已經被序列化過,當物件在本次虛擬機器中從未被序列化過,則系統將其序列化為位元組序列並輸出
- 如果某個物件在本次虛擬機器中已經序列化過,則直接輸出這個序列化編號
鑑於以上的演算法可能會造成一個潛在的問題:當序列化一個可變物件時,只有第一次使用writeObject()
方法輸出時才會輸出位元組序列,而第二次呼叫時僅僅輸出一個序列化編號,即使我們改變了這個物件的一些屬性,這些改變後的屬性也不會序列化到磁碟上,這點在開發中需要非常注意。下面我們看一下程式碼:
private static void reSerialize()
{
try(ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("student.txt"));
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("student.txt"));)
{
Student s=new Student("ben",18);
oos.writeObject(s);
Student rs1= (Student) ois.readObject();
s.setAge(32);
oos.writeObject(s);
Student rs2= (Student) ois.readObject();
System.out.println("兩個物件是否相等:"+ (rs1==rs2));
System.out.println("希望年齡變為32:"+rs2.getAge());
}catch (Exception e)
{
e.printStackTrace();
}
}
輸出結果:
兩個物件是否相等:true
希望年齡變為32:18
從輸出結果可以看出,修改前後反序列化出來的兩個物件時絕對相等的,輸出的其實是第一個物件,而且我們隊年齡做的修改也沒有生效。
自定義序列化
通過tansient阻止例項變數的序列化。
Java預設會序列化所有的例項變數,如果我們不想序列化某一個例項變數,就可以使用
tansient
這個關鍵字修飾。private transient String name;
通過writeObject()與readObject()方法控制序列化過程
只需要為實現了Serializable
介面的類提供兩個如下簽名的方法,就可完全控制序列化和發序列化過程。private void writeObject(ObjectOutputStream out) throws IOException private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException
例如我們給前面介紹的
Student
類新增兩個如下方法。private void writeObject(ObjectOutputStream out) throws IOException { out.writeObject("hello "+name); out.writeInt(age); } private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException { name= (String) in.readObject(); age=in.readInt(); }
那麼反序列化後
name
屬性的值就會加上hello
字首。通過writeReplace()方法控制序列化過程
為實現了
Serializable
介面的類提供 如下簽名的方法Any-Access-Modifier Object writeReplace() throws ObjectStreamException
該方法在開始序列化
writeObject()
之前執行,所以可以在序列化物件之前對要序列化的物件做一些處理,甚至完全替換掉原來的物件。 例如下面的程式碼無論被序列化的物件是什麼,反序列化出來的物件總是一個字串“總有刁民想害朕”。private Object writeReplace() throws ObjectStreamException{ return "總有刁民想害朕"; }
通過readResolve()方法控制反序列化過程
為實現了
Serializable
介面的類提供 如下簽名的方法Any-Access-Modifier Object readResolve() throws ObjectStreamException
該方法在反序列化
readObject()
後執行,所以可以在反序列化後對獲得的物件做一些處理,甚至完全替換為其他物件。例如下面程式碼無論反序列化後得到的物件是什麼,都會被替換成一個字串”昏君人人得而誅之”。private Object readResolve() throws ObjectStreamException{ return "昏君人人得而誅之"; }
這個函式在單例類實現序列化時特別有用,通過前面的介紹 我們知道,通過序列化可以不使用建構函式而獲取一個類的例項,這樣的話一個單例類就會存在兩個例項了,就失去效用了。那麼如何解決這個問題呢?
1、最好是使用列舉
enum
來構建一個單例,這是最好的方法,解決了序列化以及反射生成例項的問題。public enum Singleton { INSTANCE; }
2、如果只是解決由於序列化導致的單例破壞問題,可以使用
readResolve()
方法解決,如下程式碼所示:public class Singleton implements Serializable{ public static final Singleton INSTANCE = new Singleton(); private Singleton() { } protected Object readResolve() { return INSTANCE; } ... }
實現Externalizable介面
如果採用這種方式的話,序列化過程必須完全由程式設計師自己完成,看如下程式碼:
public class Teacher implements Externalizable{
private String name;
private Integer age;
public Teacher(String name,Integer age){
System.out.println("有參構造");
this.name = name;
this.age = age;
}
//setter、getter方法省略
//編寫自己的序列化邏輯
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject("hello:"+name); //將name加上字首
out.writeInt(age); //注掉這句後,age屬性將不能被序化
}
//編寫自己的反序列化邏輯
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
name = ((StringBuffer) in.readObject()).reverse().toString();
age = in.readInt();
}
@Override
public String toString() {
return "[" + name + ", " + age+ "]";
}
}
可見Externalizable
將序列化和反序列化的工作完全交給了程式設計師,那樣的好處就是自由度變大,如果碰上牛逼程式設計師,效率也會提升,碰上傻逼程式設計師就真的傻逼了。鑑於多年程式設計經驗,一般情況下還是使用Serializable
較為穩妥,和開發效率比起來,效能就是個屁,不然Java之類的語言也不會打敗C++。
結語
每次寫完一個主題的總結文章就感覺相關分知識其實不難也不多,為什麼我以前感覺那麼難那麼多呢?只能說明自己知道的還是不夠多,需要繼續努力。等以後對這部分知識有了新的認識再來更新。