Java序列化機(jī)制

什么是序列化

  • 序列化就是指對(duì)象通過(guò)寫(xiě)出描述自己狀態(tài)的數(shù)值來(lái)記錄自己的過(guò)程,將對(duì)象的運(yùn)行時(shí)數(shù)據(jù)轉(zhuǎn)化為二進(jìn)制流。
  • 反序列化是序列化的逆過(guò)程,將二進(jìn)制流轉(zhuǎn)化為對(duì)象的運(yùn)行時(shí)數(shù)據(jù)。

序列化的必要性

一切都是對(duì)象,在分布式環(huán)境中經(jīng)常需要將對(duì)象數(shù)據(jù)從這一端網(wǎng)絡(luò)或設(shè)備傳遞到另一端。這就需要有一種可以在兩端傳輸數(shù)據(jù)的協(xié)議。或者將對(duì)象持久化到硬盤(pán)上,等待下載加載數(shù)據(jù)并繼續(xù)運(yùn)行,Java序列化機(jī)制就是為了解決這樣的問(wèn)題而產(chǎn)生。

應(yīng)用場(chǎng)景

  • Java RMI調(diào)用。即Remote Method Invocation遠(yuǎn)程方法調(diào)用,它被設(shè)計(jì)成一種面向?qū)ο蟮耐ㄓ嵎绞?,允許程序員使用遠(yuǎn)程對(duì)象來(lái)實(shí)現(xiàn)通信。既然使用遠(yuǎn)程對(duì)象進(jìn)行通信,避免不了要傳輸Java對(duì)象,所以這些需要傳輸?shù)膶?duì)象必須要經(jīng)過(guò)序列化。
  • Fa?ade 模型。一種遠(yuǎn)程調(diào)用的模型。
image.png

Client 端通過(guò) Fa?ade Object 才可以與業(yè)務(wù)邏輯對(duì)象進(jìn)行交互。而客戶(hù)端的 Fa?ade Object 不能直接由 Client 生成,而是需要 Server 端生成,然后序列化后通過(guò)網(wǎng)絡(luò)將二進(jìn)制對(duì)象數(shù)據(jù)傳給 ClientClient 負(fù)責(zé)反序列化得到 Fa?ade 對(duì)象。該模式可以使得 Client 端程序的使用需要服務(wù)器端的許可,同時(shí) Client 端和服務(wù)器端的 Fa?ade Object 類(lèi)需要保持一致。當(dāng)服務(wù)器端想要進(jìn)行版本更新時(shí),只要將服務(wù)器端的 Fa?ade Object 類(lèi)的序列化 ID 再次生成,當(dāng) Client 端反序列化 Fa?ade Object 就會(huì)失敗,也就是強(qiáng)制 Client 端從服務(wù)器端獲取最新程序。

  • java對(duì)象的持久化。寫(xiě)過(guò)crud的程序員都知道,我們每天做的事情就是不停的序列化和反序列化。

如何實(shí)現(xiàn)序列化

怎樣才能正確的實(shí)現(xiàn)序列化和反序列化呢? 我們應(yīng)該考慮以下問(wèn)題:

  1. 對(duì)象的序列化,序列化的內(nèi)容是什么呢?毫無(wú)疑問(wèn),當(dāng)然是對(duì)象的運(yùn)行時(shí)數(shù)據(jù)咯,這肯定就不包含對(duì)象的類(lèi)結(jié)構(gòu),方法體等等。除非這個(gè)對(duì)象就是Class對(duì)象。哪些有事運(yùn)行時(shí)數(shù)據(jù)呢?運(yùn)行時(shí)數(shù)據(jù)包含對(duì)象的非瞬態(tài)域。其實(shí)就是對(duì)象的那些不是transient關(guān)鍵字修飾字段啦,當(dāng)然也不包含靜態(tài)字段,靜態(tài)字段是屬于類(lèi)的靜態(tài)域,又不是對(duì)象的域。

  2. 序列化和反序列化的過(guò)程是什么樣子的呢? 序列化就是將對(duì)象的運(yùn)行時(shí)數(shù)據(jù)塞到流中,然后在反序列化時(shí)創(chuàng)建一個(gè)該類(lèi)的空對(duì)象,然后將拿到的數(shù)據(jù)放到對(duì)應(yīng)的域中即可。這里可以看到序列化的類(lèi)在流的另一端是已經(jīng)存在的,靜態(tài)域當(dāng)然不需要再傳咯。靜態(tài)域已經(jīng)跟隨類(lèi)發(fā)布出去了。

  3. 哪些對(duì)象應(yīng)該被序列化,哪些對(duì)象有不應(yīng)該被序列化?在Java中一切皆對(duì)象,對(duì)象千千萬(wàn),如果每個(gè)對(duì)象都要被傳輸,影響序列化的效率暫切不說(shuō),對(duì)網(wǎng)絡(luò)和硬盤(pán)的資源也造成了浪費(fèi)啊,而且有的對(duì)象傳輸給客戶(hù)端完全沒(méi)有意義,比如說(shuō)說(shuō)我們經(jīng)常寫(xiě)的Service、Controller層對(duì)象,里面什么數(shù)據(jù)也沒(méi)有,傳到另一端也沒(méi)什么意義,而且影響效率浪費(fèi)資源。在第一個(gè)問(wèn)題中也說(shuō)明了,序列化的內(nèi)容是對(duì)象的數(shù)據(jù),數(shù)據(jù),數(shù)據(jù)。因此,含有運(yùn)行時(shí)數(shù)據(jù)的對(duì)象才值得被序列化。那哪些對(duì)象是含有數(shù)據(jù)的呢。java中用Serializable接口來(lái)標(biāo)記那些還有數(shù)據(jù)域的類(lèi)。

  4. 如何確保反序列化得到的對(duì)象與被序列化的對(duì)象是同一類(lèi)對(duì)象呢?我們希望二進(jìn)制流的兩端是一模一樣的類(lèi),如果不是或者相似,都有可能造成嚴(yán)重的錯(cuò)誤。比如說(shuō)有個(gè)相似的類(lèi)獲取到了數(shù)據(jù),但是這些數(shù)據(jù)是方法不需要的,就有可能導(dǎo)致在方法執(zhí)行時(shí)出現(xiàn)異常。解決這一問(wèn)題我們只需要在二進(jìn)制流中加入序列化類(lèi)的標(biāo)志,在反序列化時(shí),取得這個(gè)標(biāo)志,和本地類(lèi)進(jìn)行比較,如果不一致直接拋出異常,避免在后續(xù)處理中出現(xiàn)致命錯(cuò)誤。

  5. 一個(gè)應(yīng)該被序列化對(duì)象的內(nèi)部,有其他的對(duì)象引用,該怎么序列化?也就是說(shuō),若果對(duì)象的域是基本類(lèi)型,那好說(shuō)直接序列化就好了,如果是其他對(duì)象的引用怎么辦呢,如果引用的對(duì)象屬于應(yīng)該被序列化的范疇,那我們繼續(xù)遞歸的進(jìn)行序列化。如果不屬于應(yīng)該被序列化的范疇,那么拋出異常,讓程序員在這個(gè)字段上加上transient關(guān)鍵字。

  6. 一個(gè)應(yīng)該被序列化對(duì)象的內(nèi)部,會(huì)存在不該被序列化的域該怎么辦?這個(gè)好理解,像上面說(shuō)的這種情況,加上transient關(guān)鍵字即可。序列化的時(shí)候忽略他即可。

  7. 還有這樣一種場(chǎng)景如下,a對(duì)象和b對(duì)象都含有c的引用,在反序列化時(shí),怎么保證反序列化后的a、b引用的c對(duì)象是同一個(gè)呢?這個(gè)也好辦,我們?cè)诘谝淮涡蛄谢?code>c的時(shí)候,給他加一個(gè)編號(hào)#1,在序列化baField域時(shí),發(fā)現(xiàn)這個(gè)對(duì)象之前序列化過(guò)了,直接用#1代替就行了。反序列化同理。

AnObject a = new AnObject();
AnObject b = new AnObject();
AnObject c = new AnObject();
a.aField = c;
b.aField = c;
  1. 有繼承關(guān)系的對(duì)象該如何被序列化?都知道對(duì)象除了他的那些數(shù)據(jù)域以外,還有一個(gè)其父類(lèi)對(duì)象的應(yīng)用,那么序列化時(shí)父類(lèi)對(duì)象該怎么辦呢。

序列化的實(shí)踐

Java默認(rèn)序列化機(jī)制非常簡(jiǎn)單,設(shè)計(jì)者為我們屏蔽了底層實(shí)現(xiàn),對(duì)于程序員而言只需決定哪些對(duì)象需要被序列化。

//需要被序列化的類(lèi)
public class Employee implements Serializable {
    private String name;
    public Employee(String name) {
        this.name = name;
    }
}

//序列化對(duì)象
ObjectOutput objectOutput = new ObjectOutputStream(new FileOutputStream(new File("D:Temp/object.dat")));
Employee employee = new Employee("Tom");
objectOutput.writeObject(employee);

//反序列化對(duì)象
ObjectInput objectInput = new ObjectInputStream(new FileInputStream(new File("D:Temp/object.dat")));
Object readEmployee = objectInput.readObject();

System.out.println("Object read from file is " + readEmployee);

上面是一段正正經(jīng)經(jīng)的序列化和反序列化代碼,序列化后的文件能夠成功的被反序列化?,F(xiàn)在對(duì)上面的代碼做一些改變,然后看看結(jié)果。

  1. 先將Employee對(duì)象序列化到object.dat文件中,然后改變Employee的結(jié)構(gòu),看看能不能被反序列化。改變后的Employee的結(jié)構(gòu)如下。
public class Employee implements Serializable {
    private String name;
    public Employee(String name) {
        this.name = name;
    }
    // 添加一個(gè)get方法
    public void setName(String name) {
        this.name = name;
    }
}

在反序列化時(shí)拋出了異常。java.io.InvalidClassException: com.ccr.Employee; local class incompatible: stream classdesc serialVersionUID = -8417225670575338188, local class serialVersionUID = -2926582740416391491。該異常指出被序列化的文件中的serialVersionUID和本地類(lèi)的serialVersionUID不同,什么是serialVersionUID呢?可以理解為類(lèi)的版本序列號(hào),在序列化時(shí),Java會(huì)根據(jù)類(lèi)的結(jié)構(gòu),包括類(lèi)的域,類(lèi)的方法簽名等生成一個(gè)版本序列號(hào),這個(gè)版本序列號(hào)就是類(lèi)的簽名,或者是指紋信息,只要類(lèi)結(jié)構(gòu)發(fā)生任何變化,得到的序列號(hào)都會(huì)不同,雖然Employee只加了一個(gè)get方法,但是這個(gè)類(lèi)的簽名信息已經(jīng)發(fā)生變化,在反序列化時(shí),檢查他們的序列號(hào)不同,Java就認(rèn)為類(lèi)結(jié)構(gòu)發(fā)生變化,不能被反序列化。

  1. 但是我只是加了一個(gè)get方法,并沒(méi)影響類(lèi)的數(shù)據(jù)域,不是說(shuō)只是序列化數(shù)據(jù)域么,這有點(diǎn)不合理啊,確實(shí),這應(yīng)該是Java默認(rèn)序列化機(jī)制不合理的地方,好在也有補(bǔ)救辦法。我們可以通過(guò)一個(gè)靜態(tài)的final域自定義序列號(hào)版本。如:
public class Employee implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    public Employee(String name) {
        this.name = name;
    }
    // 添加一個(gè)get方法
    public void setName(String name) {
        this.name = name;
    }
}

有點(diǎn)眼熟,用eclips的童鞋都知道,工具會(huì)給我們自動(dòng)生成這玩意,他也是根據(jù)類(lèi)簽名生成的。只要保證序列化和反序列化這各類(lèi)的序列號(hào)相同,就能反序列化成功,這個(gè)可以用來(lái)控制客戶(hù)端的類(lèi)版本,使客戶(hù)端必須升級(jí)才能調(diào)用遠(yuǎn)程服務(wù)。但是問(wèn)題又來(lái)了,如果兩邊serialVersionUID相同,縱使你把這個(gè)類(lèi)改翻了天,它照樣能夠序列化成功,我相信這應(yīng)該是Java默認(rèn)序列化機(jī)制的一個(gè)缺陷。

  1. Employee的構(gòu)造函數(shù)設(shè)置為private看看反序列化的效果。這里我吧Employee默認(rèn)的構(gòu)造函數(shù)也設(shè)置成了private,反序列化依然能夠成功。這說(shuō)明反序列化沒(méi)有調(diào)用類(lèi)的構(gòu)造函數(shù),而是使用其他的方式構(gòu)造對(duì)象。
  2. 我們將Employeename域設(shè)置成transient,序列化時(shí)name = "Tom",反序列化后name = null,說(shuō)明transient阻止了name的序列化。
  3. 我們驗(yàn)證下數(shù)據(jù)域中存在引用的情況。
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;
    //引用域
    private String name;

    Employee leader;

    public Employee(String name) {
        this.name = name;
    }

}


ObjectOutput objectOutput = new ObjectOutputStream(new FileOutputStream(new File("D:Temp/object.dat")));
Employee employee = new Employee("Tom");
//將引用域設(shè)置成自己,觀察會(huì)不會(huì)相同對(duì)象重復(fù)序列化
employee.leader = employee;
objectOutput.writeObject(employee);

//反序列化時(shí)觀察readEmployee對(duì)象leader域是readEmployee本省
ObjectInput objectInput = new ObjectInputStream(new FileInputStream(new File("D:Temp/object.dat")));
Object readEmployee = objectInput.readObject();
  1. 看看繼承的情況,如果父類(lèi)被標(biāo)記為可序列化,那么子類(lèi)必然可以版序列化。如果子類(lèi)被標(biāo)記為可序列化,父類(lèi)沒(méi)有被標(biāo)記為序列化那又是什么情況呢。
//父類(lèi)不可以被序列化
public class Employee {

    private static final long serialVersionUID = 1L;

    String name;

    public void say(){
        System.out.println("I'm father object,my name is " + name);
    }

}

//子類(lèi)可以被序列化
public class Manager extends Employee implements Serializable {

    @Override
    public void say(){
        System.out.println("I'm son object,my name is " + name);
    }
}

//序列化
ObjectOutput objectOutput = new ObjectOutputStream(new FileOutputStream(new File("D:Temp/object.dat")));

Manager manager = new Manager();
manager.name = "Tom";
manager.say();

objectOutput.writeObject(manager);

//反序列化
ObjectInput objectInput = new ObjectInputStream(new FileInputStream(new File("D:Temp/object.dat")));

Manager readEmployee = (Manager) objectInput.readObject();
readEmployee.say();

//結(jié)果
I'm son object,my name is Tom
I'm son object,my name is null

可以看出這種父類(lèi)不可序列化,子類(lèi)可序列化的情況,Java的默認(rèn)處理機(jī)制是將父類(lèi)域的數(shù)據(jù)丟棄,只序列化子類(lèi)域的數(shù)據(jù)。

  1. 自定義序列化。Java提供了兩種方式來(lái)供我們自己定義對(duì)象的序列化方式。一種是定義private void writeObject 和private void readObject方法,另一種是實(shí)現(xiàn)Externalizable接口。
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;

    String name;

    transient int age;

    public void say(){
        System.out.println("my name is " + name + ". and I'm " + age + " years old.");
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        //調(diào)用默認(rèn)的序列化機(jī)制,該機(jī)制會(huì)忽略age字段
        out.defaultWriteObject();
        //使用自己的方式來(lái)實(shí)現(xiàn)age字段的序列化
        out.writeInt(age);
    }
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        age = in.readInt();
    }

}

//序列化
ObjectOutput objectOutput = new ObjectOutputStream(new FileOutputStream(new File("D:Temp/object.dat")));

Employee employee = new Employee();
employee.name = "Tom";
employee.age = 41;
employee.say();

objectOutput.writeObject(employee);

//反序列化
ObjectInput objectInput = new ObjectInputStream(new FileInputStream(new File("D:Temp/object.dat")));

Employee readEmployee = (Employee) objectInput.readObject();

readEmployee.say();

//結(jié)果
my name is Tom. and I'm 41 years old.
my name is Tom. and I'm 41 years old.

public class Employee implements Externalizable {

    private static final long serialVersionUID = 1L;

    String name;

    int age;

    public void say(){
        System.out.println("my name is " + name + ". and I'm " + age + " years old.");
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        //我們自決定哪些字段該序列化哪些字段不該序列化,或者該怎樣序列化
        //也可以調(diào)用默認(rèn)的機(jī)制
        //可以在這里進(jìn)行加密
        //也可以用這種機(jī)制實(shí)現(xiàn)超類(lèi)的具體化,如果超類(lèi)沒(méi)有實(shí)現(xiàn)Serializable
        out.writeObject("aa" + name);
        out.writeInt(age + 1);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        //我們自決定哪些字段該反序列化哪些字段不該反序列化
        name = (String) in.readObject();
        age = in.readInt() - 1;
    }
}


//序列化
ObjectOutput objectOutput = new ObjectOutputStream(new FileOutputStream(new File("D:Temp/object.dat")));

Employee employee = new Employee();
employee.name = "Tom";
employee.age = 41;
employee.say();

objectOutput.writeObject(employee);

//反序列化
ObjectInput objectInput = new ObjectInputStream(new FileInputStream(new File("D:Temp/object.dat")));

Employee readEmployee = (Employee) objectInput.readObject();

readEmployee.say();

//結(jié)果
my name is Tom. and I'm 41 years old.
my name is aaTom. and I'm 41 years old.

參考這篇文章很詳細(xì)

總結(jié)

寫(xiě)了那么多,了解序列化無(wú)非下面兩點(diǎn):

  1. 序列化的內(nèi)容是對(duì)象的數(shù)據(jù)。有用的對(duì)象數(shù)據(jù)。但是大部分對(duì)象要么是無(wú)狀態(tài)的,要么是輔助數(shù)據(jù)。這就需要程序員。來(lái)決定哪些有用的數(shù)據(jù)對(duì)象需要被序列化。這是我思考序列化機(jī)制的初衷,為什么對(duì)象序列化需要Serializable接口標(biāo)記。
  2. 知道了序列化的內(nèi)容,下面該思考的就是序列化的方式。就是對(duì)象自己控制該怎么序列化,序列化哪些內(nèi)容。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Java序列化機(jī)制 序列化和反序列化 Java序列化是Java內(nèi)建的數(shù)據(jù)(對(duì)象)持久化機(jī)制,通過(guò)序列化可以將運(yùn)行時(shí)...
    0x70e8閱讀 506評(píng)論 0 1
  • java的序列化機(jī)制支持將對(duì)象序列化為本地文件或者通過(guò)網(wǎng)絡(luò)傳輸至別處, 而反序列化則可以讀取流中的數(shù)據(jù), 并將其轉(zhuǎn)...
    Ten_Minutes閱讀 745評(píng)論 0 1
  • JAVA序列化機(jī)制的深入研究 對(duì)象序列化的最主要的用處就是在傳遞,和保存對(duì)象(object)的時(shí)候,保證對(duì)象的完整...
    時(shí)待吾閱讀 11,164評(píng)論 0 24
  • 最近有開(kāi)發(fā)者發(fā)現(xiàn)蘋(píng)果提醒a(bǔ)pple pay證書(shū)要過(guò)期了,如果點(diǎn)激活了(Activate),會(huì)影響線(xiàn)上Applepa...
    Applet閱讀 4,368評(píng)論 0 4
  • 昨日,有一認(rèn)識(shí)多年的老灸友發(fā)微信給我,說(shuō)前天幫她灸完后回去,全身過(guò)敏,她問(wèn)我是不是給她調(diào)亂了自身免疫力。 依我多...
    艾_一直在閱讀 991評(píng)論 0 1

友情鏈接更多精彩內(nèi)容