Java基礎——序列化

it_was發表於2020-09-29

引自《Java 程式設計思想》

當建立物件時,在程式執行期間可以獲取,但是程式終止時,所有的物件都會被清除,我們是無法再獲取的。當然,你可以透過將資訊寫入檔案或者資料庫來達到目的。但是為了更方便,Java為我們提供了序列化機制,並且遮蔽了大部分細節。 ——Bruce Eckel

Java提供序列化作用:

  • Java的遠端方法呼叫-RMI。
  • Java Beans的使用。

使用Java物件序列化,在儲存物件時,會把其狀態儲存為一組位元組,在未來,再將這些位元組組裝成物件。必須注意地是,物件序列化儲存的是物件的”狀態”,即它的成員變數。由此可知,物件序列化不會關注類中的靜態變數。:boom:

2.1 序列化機制一: Serializable 介面:boom:

Serializable介面是最簡單的實現序列化的方式,如果類實現了Serializable介面,那麼就可以表明該類的物件可被序列化!否則就會報錯。常見的預設實現該介面的有所有基本型別的封裝型別,String,容器類甚至Class 物件!

//可以看到原始碼中Serializable介面內部沒有任何東西,只是一個標識!
public interface Serializable {
    //nothing!
}

序列化與反序列化方法:呼叫 ObjectOutputStream 和 ObjectInputStream :facepunch:

package ddx.序列化;

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person = new Person(18,"ddx");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.out") );
        out.writeObject("person storage\n");
        out.writeObject(person);
        out.close();
        System.out.print("序列化"+"person storage\n"+ person);

        ObjectInputStream in  = new ObjectInputStream(new FileInputStream("person.out"));
        String title = (String)in.readObject(); //注意反序列化順序與序列化順序保持一致
        Person person1 = (Person) in.readObject();
        in.close();
        System.out.print("反序列化"+title+ person1);
    }
}
class Person implements Serializable{
    public int age;
    public String name;
    Person(int age , String s){
        this.age = age;
        this.name = s;
    }

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

執行結果

序列化person storage
Person{age=18, name='ddx'}
反序列化person storage
Person{age=18, name='ddx'}

輸出檔案

Java基礎——序列化

注意點:再強調一下,物件序列化的主體是成員變數,而不包括靜態變數:exclamation::exclamation::exclamation: 一個類序列化,另一個類反序列化,分別執行!
ackage ddx.序列化;

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person = new Person(18,"ddx");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.out") );
        out.writeObject("person storage\n");
        out.writeObject(person);
        out.close();
        System.out.println("序列化"+"person storage\n"+ person);
    }
}
class Person implements Serializable{
    public int age;
    public String name;
    public static int count = 0;
    Person(int age , String s){
        this.age = age;
        this.name = s;
        count++;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ",count='"+count+'\'' +
                '}';
    }
}
package ddx.序列化;

import java.io.*;

public class test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.out"));
        String title = (String) in.readObject();
        Person person1 = (Person) in.readObject();
        in.close();
        System.out.println("反序列化" + title + person1);
    }
}
序列化person storage
Person{age=18, name='ddx',count='1'}
反序列化person storage
Person{age=18, name='ddx',count='0'}

//透過以上輸出可以發現序列化不對靜態變數做任何操作,只會保留初始值!!!!

2.2 Serializable 的序列化規則

使用預設機制,在序列化物件時,不僅會序列化當前物件本身,還會對該物件引用的其它物件也進行序列化,同樣地,這些其它物件引用的另外物件也將被序列化,以此類推。這種情況被稱為物件網所以,如果一個物件包含的成員變數是容器類物件,而這些容器所含有的元素也是容器類物件,那麼這個序列化的過程就會較複雜,開銷也較大。

2.3 Externalizable介面

Externalizable繼承於Serializable,當使用該介面時,序列化的細節需要由程式設計師去完成。

兩者的區別除了上面需要手動完成還包括下面這點::raising_hand:

  • 使用Serializable介面的物件反序列化恢復物件時,是完全以儲存它的二進位制位構造的
  • 而使用Externalizable 介面的物件反序列化恢復物件時,必須至少要有一個預設的公共的建構函式!!!!然後呼叫readExternal方法!
public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException,ClassNotFoundException;
}

演示程式碼:

package ddx.序列化;

import java.io.*;

public class Main1 {
    public static void main(String[] args ) throws IOException, ClassNotFoundException {
        info info1 = new info(10,"ddx");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("info.out"));
        out.writeObject(info1);
        System.out.println("序列化之前\n"+info1);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("info.out"));
        info info2 = (info) in.readObject();
        System.out.println("序列化之後\n"+info2);
    }
}
class info implements Externalizable{
    public int id;
    public String name;
    public info(int id, String name){
        this.id = id;
        this.name = name;
        System.out.println("info正在執行帶參建構函式");
    }

    public info(){
        System.out.println("info正在執行無參建構函式");
    }
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(this.id);
        out.writeObject(name);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        //1
        //in.readInt();
        //in.readObject();
        id = in.readInt();
        name = (String)in.readObject();
    }

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

2.4 transient 關鍵字

使用transient關鍵字選擇不需要序列化的欄位。比如一些敏感的私有的資訊,可以選擇使用改關鍵字,虛擬機器會忽略該欄位!!!:boom:

package ddx.序列化;

import java.io.*;

public class Main2 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Message  message = new Message(201792237,533534);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Self_Message.out"));
        out.writeObject(message);
        out.close();
        System.out.println("序列化之前" + message);

        ObjectInputStream  in = new ObjectInputStream(new FileInputStream("Self_Message.out"));
        Message message1 = (Message) in.readObject();
        System.out.println("序列化之後" + message1);
    }
}
class Message implements Serializable {
    private int id;
    private transient int password; //transient 關鍵字修飾!!!
    Message(int id, int password){
        this.id = id;
        this.password = password;
    }

    @Override
    public String toString() {
        return "message{" +
                "id=" + id +
                ", password=" + password +
                '}';
    }
}
序列化之前message{id=201792237, password=533534}
序列化之後message{id=201792237, password=0}

從輸出我們看到,使用transient修飾的屬性,java序列化時,會忽略掉此欄位,所以反序列化出的物件,被transient修飾的屬性是預設值。對於引用型別,值是null;基本型別,值是0;boolean型別,值是false。:boom::boom::boom:

2.5 序列化永續性

同一物件序列化多次,會將這個物件序列化多次嗎?答案是否定的。

package ddx.序列化;

import java.io.*;

public class Main3 {

    public static void main(String[] args) throws Exception {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {
            Person person = new Person(20,"路飛");
            Teacher t1 = new Teacher("雷利", person);
            Teacher t2 = new Teacher("紅髮香克斯", person);
            //依次將4個物件寫入輸入流
            oos.writeObject(person);
            oos.writeObject(t1);
            oos.writeObject(t2);
            oos.writeObject(t2);//重複序列化同一個物件!!!!!!

            oos.close();

            ObjectInputStream in = new ObjectInputStream(new FileInputStream("teacher.txt"));
            Person p = (Person) in.readObject();
            Teacher tt1 = (Teacher)in.readObject();
            Teacher tt2 = (Teacher)in.readObject();
            Teacher tt3 = (Teacher)in.readObject();
            System.out.println(p);
            System.out.println(tt1);
            System.out.println(tt2);
            System.out.println(tt3);
        }
    }

}
class Teacher implements Serializable{
    public  String name;
    public Person person;
    Teacher(String name, Person person){
        this.name = name;
        this.person = person;
    }

    @Override
    public String toString() {
        return "Teacher{" +super.toString()+
                "name='" + name + '\'' +
                ", person=" + person +
                '}';
    }
}
正在初始化
ddx.序列化.Person@6f496d9f
Teacher{ddx.序列化.Teacher@723279cf  name='雷利', person=ddx.序列化.Person@6f496d9f}
Teacher{ddx.序列化.Teacher@10f87f48  name='紅髮香克斯', person=ddx.序列化.Person@6f496d9f}
Teacher{ddx.序列化.Teacher@10f87f48  name='紅髮香克斯', person=ddx.序列化.Person@6f496d9f}
//可以發現person並沒有被多次序列化,而t2也只被序列化了一次!!!地址都相同!

從輸出結果可以看出,Java序列化同一物件,並不會將此物件序列化多次得到多個物件。:eyes:

Java序列化演算法

  1. 所有儲存到磁碟的物件都有一個序列化編碼號
  2. 當程式試圖序列化一個物件時,會先檢查此物件是否已經序列化過,只有此物件從未(在此虛擬機器)被序列化過,才會將此物件序列化為位元組序列輸出。
  3. 如果此物件已經序列化過,則直接輸出編號即可。

圖示上述序列化過程(順序不同!原理一致)

我們知道,反序列化必須擁有class檔案,但隨著專案的升級,class檔案也會升級,序列化怎麼保證升級前後的相容性呢?

java序列化提供了一個private static final long serialVersionUID 的序列化版本號,只有版本號相同,即使更改了序列化屬性,物件也可以正確被反序列化回來。

public class Person implements Serializable {
    //序列化版本號
    private static final long serialVersionUID = 1111013L;
    private String name;
    private int age;
    //省略構造方法及get,set
}

如果反序列化使用的class的版本號與序列化時使用的不一致,反序列化會報InvalidClassException異常。

序列化版本號可自由指定,如果不指定,JVM會根據類資訊自己計算一個版本號,這樣隨著class的升級,就無法正確反序列化;不指定版本號另一個明顯隱患是,不利於jvm間的移植,可能class檔案沒有更改,但不同jvm可能計算的規則不一樣,這樣也會導致無法反序列化。

什麼情況下需要修改serialVersionUID呢?分三種情況:boom::boom::boom:

  • 如果只是修改了方法,反序列化不容影響,則無需修改版本號;
  • 如果只是修改了靜態變數,瞬態變數(transient修飾的變數),反序列化不受影響,無需修改版本號;
  • 如果修改了非瞬態變數,則可能導致反序列化失敗。如果新類中例項變數的型別與序列化時類的型別不一致,則會反序列化失敗,這時候需要更改serialVersionUID。如果只是新增了例項變數,則反序列化回來新增的是預設值;如果減少了例項變數,反序列化時會忽略掉減少的例項變數。
  1. 所有需要網路傳輸的物件都需要實現序列化介面,透過建議所有的javaBean都實現Serializable介面。
  2. 物件的類名、例項變數(包括基本型別,陣列,對其他物件的引用)都會被序列化;方法、類變數、transient例項變數都不會被序列化。
  3. 如果想讓某個變數不被序列化,使用transient修飾。
  4. 序列化物件的引用型別成員變數,也必須是可序列化的,否則,會報錯。
  5. 反序列化時必須有序列化物件的class檔案。
  6. 當透過檔案、網路來讀取序列化後的物件時,必須按照實際寫入的順序讀取。
  7. 單例類序列化,需要重寫readResolve()方法;否則會破壞單例原則。
  8. 同一物件序列化多次,只有第一次序列化為二進位制流,以後都只是儲存序列化編號,不會重複序列化。
  9. 建議所有可序列化的類加上serialVersionUID 版本號,方便專案升級。
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章