版權申明】非商業目的可自由轉載
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/80629348
出自:shusheng007
概述
什麼是序列化?什麼是反序列化?爲什麼需要序列化?如何序列化?應該注意什麼?本文將從這幾方面來論述。
定義
什麼是序列化?什麼是反序列化?
序列化: 把Java對象轉換爲字節序列的過程。
反序列化:把字節序列恢復爲Java對象的過程。
作用
爲什麼需要序列化?
在當今的網絡社會,我們需要在網絡上傳輸各種類型的數據,包括文本、圖片、音頻、視頻等, 而這些數據都是以二進制序列的形式在網絡上傳送的,那麼發送方就需要將這些數據序列化爲字節流後傳輸,而接收方接到字節流後需要反序列化爲相應的數據類型。當然接收方也可以將接收到的字節流存儲到磁盤中,等到以後想恢復的時候再恢復。
綜上,可以得出對象的序列化和反序列化主要有兩種用途:
- 把對象的字節序列永久地保存到磁盤上。(持久化對象)
- 可以將Java對象以字節序列的方式在網絡中傳輸。(網絡傳輸對象)
如何實現
如何序列化和反序列化?
如果要讓某個對象支持序列化機制,則其類必須實現下面這兩個接口中任一個。
Serializable
public interface Serializable { }
Externalizable
public interface Externalizable extends java.io.Serializable { void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; }
實現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++。
結語
每次寫完一個主題的總結文章就感覺相關分知識其實不難也不多,爲什麼我以前感覺那麼難那麼多呢?只能說明自己知道的還是不夠多,需要繼續努力。等以後對這部分知識有了新的認識再來更新。
參考文章: https://blog.csdn.net/zcl_love_wx/article/details/52126876