Java序列化

含義、意義使用場(chǎng)景

序列化:將對(duì)象寫入到IO流中

反序列化:從IO流中恢復(fù)對(duì)象

意義:序列化機(jī)制允許將實(shí)現(xiàn)序列化的Java對(duì)象轉(zhuǎn)換位字節(jié)序列,這些字節(jié)序列可以保存在磁盤上,或通過網(wǎng)絡(luò)傳輸,以達(dá)到以后恢復(fù)成原來的對(duì)象。序列化機(jī)制使得對(duì)象可以脫離程序的運(yùn)行而獨(dú)立存在。

使用場(chǎng)景:所有可在網(wǎng)絡(luò)上傳輸?shù)膶?duì)象都必須是可序列化的,比如RMI(remote method invoke,即遠(yuǎn)程方法調(diào)用),傳入的參數(shù)或返回的對(duì)象都是可序列化的,否則會(huì)出錯(cuò);所有需要保存到磁盤的java對(duì)象都必須是可序列化的。通常建議:程序創(chuàng)建的每個(gè)JavaBean類都實(shí)現(xiàn)Serializeable接口。

序列化的實(shí)現(xiàn)方式

如果需要將某個(gè)對(duì)象保存到磁盤上或者通過網(wǎng)絡(luò)傳輸,那么這個(gè)類應(yīng)該實(shí)現(xiàn)Serializable接口或者Externalizable接口之一。

Serializable

普通序列化

Serializable接口是一個(gè)標(biāo)記接口,不用實(shí)現(xiàn)任何方法。一旦實(shí)現(xiàn)了此接口,該類的對(duì)象就是可序列化的。

創(chuàng)建對(duì)象類

public class Person implements Serializable {
    private String name;
    private int age;   
    
    //我不提供無參構(gòu)造器    

    public Person(String name, int age) {
        System.out.println("調(diào)用有參構(gòu)造方法");
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + ",    age=" + age + '}';
    }
}

序列化步驟

步驟一:創(chuàng)建一個(gè)ObjectOutputStream輸出流;

步驟二:調(diào)用ObjectOutputStream對(duì)象的writeObject輸出可序列化對(duì)象

public class WriteObject {
    public static void main(String[] args) {
        try {
            //創(chuàng)建一個(gè)ObjectOutputStream輸出流 
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt")); 
            //將對(duì)象序列化到文件s
            Person person = new Person("9龍", 23);
            oos.writeObject(person);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
// 輸出結(jié)果:
調(diào)用有參構(gòu)造方法

反序列化步驟

步驟一:創(chuàng)建一個(gè)ObjectInputStream輸入流;

步驟二:調(diào)用ObjectInputStream對(duì)象的readObject()得到序列化的對(duì)象。

我們將上面序列化到person.txt的person對(duì)象反序列化回來

public class WriteObject {
    public static void main(String[] args) {
        try {
            //創(chuàng)建一個(gè)ObjectOutputStream輸出流 
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.txt"));
            Person brady = (Person) ois.readObject();
            System.out.println(brady);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//輸出結(jié)果:
Person{name='9龍', age=23}

輸出告訴我們,反序列化并不會(huì)調(diào)用構(gòu)造方法。反序列的對(duì)象是由JVM自己生成的對(duì)象,不通過構(gòu)造方法生成。

成員為引用的序列化

如果一個(gè)可序列化的類的成員不是基本類型,也不是String類型,那這個(gè)引用類型也必須是可序列化的;否則,會(huì)導(dǎo)致此類不能序列化。

看例子,我們新增一個(gè)Teacher類。將Person去掉實(shí)現(xiàn)Serializable接口代碼

public class Person {
    // 省略。與上邊代碼相同
}

public class Teacher implements Serializable {
    private String leval;
    private Person person;

    public Teacher(String leval, Person person) {
        this.leval = leval;
        this.person = person;
    }

    public static void main(String[] args) throws Exception {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {
            Person person = new Person("路飛", 20);
            Teacher teacher = new Teacher("雷利", person);
            oos.writeObject(teacher);
        }
    }
}

控制臺(tái)輸出:

調(diào)用有參構(gòu)造方法
Exception in thread "main" java.io.NotSerializableException: com.jelly.java.serialize.Person
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
    at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
    at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
    at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
    at com.jelly.java.serialize.Teacher.main(Teacher.java:20)

因?yàn)镻erson類的對(duì)象是不可序列化的,這導(dǎo)致了Teacher的對(duì)象不可序列化

對(duì)同一個(gè)對(duì)象序列化多次

  public static void writeMutObject() {
        try {
            //創(chuàng)建一個(gè)ObjectOutputStream輸出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
            //將對(duì)象序列化到文件s
            Person p1 = new Person("1龍", 23);
            Person p2 = new Person("9龍", 30);

            oos.writeObject(p1);
            oos.writeObject(p2);
            oos.writeObject(p2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void readMutObject() {
        try {
            //創(chuàng)建一個(gè)ObjectOutputStream輸出流
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
            Person p1 = (Person) ois.readObject();
            Person p2 = (Person) ois.readObject();
            Person p3 = (Person) ois.readObject();
            System.out.println(p1);
            System.out.println(p2);
            System.out.println(p3);
            System.out.println("p1==p2? :" + p1.equals(p2));
            System.out.println("p2==p3? :" + p2.equals(p3));
            System.out.println("p1==p3? :" + p1.equals(p3));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
//        writeMutObject();
        readMutObject();
    }

輸出

Person{name='1龍',    age=23}
Person{name='9龍',    age=30}
Person{name='9龍',    age=30}
p1==p2? :false
p2==p3? :true
p1==p3? :false

從輸出結(jié)果可以看出,Java序列化同一對(duì)象,并不會(huì)將此對(duì)象序列化多次得到多個(gè)對(duì)象

序列化算法

  • 所有保存到磁盤的對(duì)象都有一個(gè)序列化編碼號(hào)
  • 當(dāng)程序試圖序列化一個(gè)對(duì)象時(shí),會(huì)先檢查此對(duì)象是否已經(jīng)序列化過,只有此對(duì)象從未(在此虛擬機(jī))被序列化過,才會(huì)將此對(duì)象序列化為字節(jié)序列。
  • 如果此對(duì)象已經(jīng)序列化過,則直接存儲(chǔ)對(duì)應(yīng)的編號(hào)即可。

序列化算法潛在問題

由于java序利化算法不會(huì)重復(fù)序列化同一個(gè)對(duì)象,只會(huì)記錄已序列化對(duì)象的編號(hào)。如果序列化一個(gè)可變對(duì)象(對(duì)象內(nèi)的內(nèi)容可更改)后,更改了對(duì)象內(nèi)容,再次序列化,并不會(huì)再次將此對(duì)象轉(zhuǎn)換為字節(jié)序列,而只是保存序列化編號(hào),那么就會(huì)造成數(shù)據(jù)丟失。

public static void execp() {
    try {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
        //將對(duì)象序列化到文件s
        Person p1 = new Person("1龍", 23);
        oos.writeObject(p1);
        p1.setName("9龍");
        oos.writeObject(p1);
        oos.close();
        oos.flush();
          創(chuàng)建一個(gè)ObjectOutputStream輸出流
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.txt"));
        Person pp1 = (Person) ois.readObject();
        Person pp2 = (Person) ois.readObject();
        System.out.println(pp1);
        System.out.println(pp2);
    } catch (Exception e) {
        System.out.println("exception " + e);
    }
}

public static void main(String[] args) {
    execp();
}

// 輸出:
調(diào)用有參構(gòu)造方法
Person{name='1龍',    age=23}
Person{name='1龍',    age=23}

定制序列化方式

序列化時(shí)忽略字段 (transient)

有些時(shí)候,我們有這樣的需求,某些屬性不需要序列化。使用transient關(guān)鍵字選擇不需要序列化的字段。

使用transient修飾的屬性,java序列化時(shí),會(huì)忽略掉此字段,所以反序列化出的對(duì)象,被transient修飾的屬性是默認(rèn)值。對(duì)于引用類型,值是null;基本類型,值是0;boolean類型,值是false。

自定義序列化方法

使用transient雖然簡(jiǎn)單,但將此屬性完全隔離在了序列化之外。java提供了可選的??梢赃M(jìn)行控制序列化的方式,或者對(duì)序列化數(shù)據(jù)進(jìn)行編碼加密等。

private void writeObject (java.io.ObjectOutputStream out)    throws IOException;
private void readObject (java.io.ObjectIutputStream in)    throws IOException, ClassNotFoundException;
private void readObjectNoData ()    throws ObjectStreamException;

通過重寫writeObject與readObject方法,可以自己選擇哪些屬性需要序列化,哪些屬性不需要。如果writeObject使用某種規(guī)則序列化,則相應(yīng)的readObject需要相反的規(guī)則反序列化,以便能正確反序列化出對(duì)象。

這里展示對(duì)名字進(jìn)行反轉(zhuǎn)加密。

public class Person implements Serializable {
    private String name;
    private int age;

    //將名字反轉(zhuǎn)寫入二進(jìn)制流
    private void writeObject(ObjectOutputStream out) throws IOException {            
        out.writeObject(new StringBuffer(this.name).reverse());
        out.writeInt(age);
    }

    //將讀出的字符串反轉(zhuǎn)恢復(fù)回來      
    private void readObject(ObjectInputStream ins) throws IOException, ClassNotFoundException {       
        this.name = ((StringBuffer) ins.readObject()).reverse().toString();
        this.age = ins.readInt();
    }
}

當(dāng)序列化流不完整時(shí),readObjectNoData()方法可以用來正確地初始化反序列化的對(duì)象。例如,使用不同類接收反序列化對(duì)象,或者序列化流被篡改時(shí),系統(tǒng)都會(huì)調(diào)用readObjectNoData()方法來初始化反序列化的對(duì)象。

Externalizable

通過實(shí)現(xiàn)Externalizable接口,必須實(shí)現(xiàn)writeExternal、readExternal方法。

public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;

    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

public class ExPerson implements Externalizable {
    private String name;
    private int age;    
    
    //注意,必須加上pulic 無參構(gòu)造器
    public ExPerson() {
    }

    public ExPerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //將name反轉(zhuǎn)后寫入二進(jìn)制流       
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        StringBuffer reverse = new StringBuffer(name).reverse();
        System.out.println(reverse.toString());
        out.writeObject(reverse);
        out.writeInt(age);
    }

    //將讀取的字符串反轉(zhuǎn)后賦值給name實(shí)例變量       
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = ((StringBuffer) in.readObject()).reverse().toString();
        System.out.println(name);
        this.age = in.readInt();
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ExPerson.txt")); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ExPerson.txt"))) {
            oos.writeObject(new ExPerson("brady", 23));
            ExPerson ep = (ExPerson) ois.readObject();
            System.out.println(ep);
        }
    }
}
//輸出結(jié)果
ydarb 
brady
ExPerson{name='brady', age=23}

注意:Externalizable接口不同于Serializable接口,實(shí)現(xiàn)此接口必須實(shí)現(xiàn)接口中的兩個(gè)方法實(shí)現(xiàn)自定義序列化,這是強(qiáng)制性的;特別之處是必須提供pulic的無參構(gòu)造器,因?yàn)樵诜葱蛄谢臅r(shí)候需要反射創(chuàng)建對(duì)象。

二者對(duì)比

實(shí)現(xiàn)Serializable接口 實(shí)現(xiàn)Externalizable接口
系統(tǒng)自動(dòng)存儲(chǔ)必要的信息 程序員決定存儲(chǔ)哪些信息
Java內(nèi)建支持,易于實(shí)現(xiàn),只需要實(shí)現(xiàn)該接口即可,無需任何代碼支持 必須實(shí)現(xiàn)接口內(nèi)的兩個(gè)方法
性能略差 性能略好

序列化版本號(hào)serialVersionUID

我們知道,反序列化必須擁有class文件,但隨著項(xiàng)目的升級(jí),class文件也會(huì)升級(jí),序列化怎么保證升級(jí)前后的兼容性呢?
java序列化提供了一個(gè)

private static final long serialVersionUID  = -81298930239;

的序列化版本號(hào),只有版本號(hào)相同,即使更改了序列化屬性,對(duì)象也可以正確被反序列化回來。

如果反序列化使用的class的版本號(hào)與序列化時(shí)使用的不一致,反序列化會(huì)報(bào)InvalidClassException異常。

序列化版本號(hào)指定

可以自由指定,如果不指定JVM會(huì)根據(jù)類信息自己計(jì)算一個(gè)版本號(hào),這樣隨著class的升級(jí),就無法正確反序列化;不指定版本號(hào)另一個(gè)明顯隱患是,不利于jvm間的移植,可能class文件沒有更改,但不同jvm可能計(jì)算的規(guī)則不一樣,這樣也會(huì)導(dǎo)致無法反序列化。

什么情況下需要修改serialVersionUID呢?

分三種情況。

  1. 如果只是修改了方法,反序列化不影響,則無需修改版本號(hào);
  2. 如果只是修改了靜態(tài)變量,瞬態(tài)變量(transient修飾的變量),反序列化不受影響,無需修改版本號(hào);
  3. 如果修改了非瞬態(tài)變量,則可能導(dǎo)致反序列化失敗。如果新類中實(shí)例變量的類型與序列化時(shí)類的類型不一致,則會(huì)反序列化失敗,這時(shí)候需要更改serialVersionUID。如果只是新增了實(shí)例變量,則反序列化回來新增的是默認(rèn)值;如果減少了實(shí)例變量,反序列化時(shí)會(huì)忽略掉減少的實(shí)例變量。

總結(jié)

  1. 所有需要網(wǎng)絡(luò)傳輸?shù)膶?duì)象都需要實(shí)現(xiàn)序列化接口,通過建議所有的javaBean都實(shí)現(xiàn)Serializable接口。
  2. 對(duì)象的類名、實(shí)例變量(包括基本類型,數(shù)組,對(duì)其他對(duì)象的引用)都會(huì)被序列化;方法、類變量、transient實(shí)例變量都不會(huì)被序列化。
  3. 如果想讓某個(gè)變量不被序列化,使用transient修飾。
  4. 序列化對(duì)象的引用類型成員變量,也必須是可序列化的,否則,會(huì)報(bào)錯(cuò)。
  5. 反序列化時(shí)必須有序列化對(duì)象的class文件。
  6. 當(dāng)通過文件、網(wǎng)絡(luò)來讀取序列化后的對(duì)象時(shí),必須按照實(shí)際寫入的順序讀取。
  7. 單例類序列化,需要重寫readResolve()方法;否則會(huì)破壞單例原則。
  8. 同一對(duì)象序列化多次,只有第一次序列化為二進(jìn)制流,以后都只是保存序列化編號(hào),不會(huì)重復(fù)序列化。
  9. 建議所有可序列化的類加上serialVersionUID 版本號(hào),方便項(xiàng)目升級(jí)。
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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