天天看點

【Java】對象的序列化和克隆詳解

前言

在學習源碼的過程中,常常看到很多類都實作了Cloneable接口或是Serializable接口,如集合類。雖然知道他們的作用是能進行對象序列化或者克隆,但是具體的功能卻還是一知半解,是以花了些時間去系統地了解了一下他們。

正文

克隆也經常被稱為拷貝(copy),比如很多面試官都會問深拷貝和淺拷貝,就是深克隆和淺克隆。

序列化和克隆

序列化 - Serializable

定義:将實作了Serializable接口(标記型接口)的對象轉換成一個位元組數組,并可以将該位元組數組轉為原來的對象。

簡介

為什麼我們需要序列化機制?

我們可以在JVM運作時随時使用我們的對象,因為他們都存放在JVM的堆記憶體裡。但是一旦JVM停止運作,那麼這些對象就全部沒有了。

有很多情況,我們可能需要将對象持久化,然後在之後的某個時間,或者在另一台JVM上将其重新讀取出來。我們需要一種通用的轉換對象、讀取對象的方式,于是Java的序列化應運而生。

對象序列化機制(object serialization)是Java語言内建的一種對象持久化方式,通過對象序列化,可以把對象的狀态儲存為位元組數組,并且可以在有需要的時候将這個位元組數組通過反序列化的方式再轉換成對象。對象序列化可以很容易的在JVM中的活動對象和位元組數組(流)之間進行轉換。

在Java中,對象的序列化與反序列化被廣泛應用到RMI(遠端方法調用)及網絡傳輸中。

ObjectOutputStream

是專門用來輸出對象的輸出流;

ObjectOutputStream

将 Java 對象寫入 OutputStream。然後可以使用

ObjectInputStream

讀取(重構)對象。

執行個體

假設我們有一個Person類,現在我們要寫一個序列化方法把Person類序列化到一個txt檔案中,然後寫一個反序列化方法,将Person對象從txt檔案中讀取。

Person類

public class Person implements Serializable {

    private static final long serialVersionUID = -5483049468519854281L;  //手動指定UID

    String name;
    String sex;
    int age;

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

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

}

           

要進行序列化,該類必須要實作

Serializable

接口。

方法類

public class SerializeTest {

	//序列化方法,将person對象序列化到person.txt檔案
    public static void serializePerson() throws IOException {
        Person person = new Person("jack", "男", 20);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("person.txt")));
        oos.writeObject(person);
        System.out.println("序列化person成功.");
    }

	//反序列化方法,将person對象從person.txt檔案中讀取
    public static Person deserializePerson() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("person.txt")));
        Person person = (Person) ois.readObject();
        return person;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        serializePerson();
        Person person = deserializePerson();
        System.out.println(person.toString());
    }
}

           

輸出結果:

【Java】對象的序列化和克隆詳解

說明序列化是成功的。

關于serialVersionUID

這裡還要說一下為什麼Person類中要設定

serialVersionUID

Java的序列化機制是通過在運作時判斷類的

serialVersionUID

來驗證版本一緻性的。在進行反序列化時,JVM會把傳來的位元組流中的

serialVersionUID

與本地相應實體(類)的

serialVersionUID

進行比較,如果相同就認為是一緻的,可以進行反序列化,否則就會出現序列化版本不一緻的異常。

(InvalidCastException)

1)如果沒有添加

serialVersionUID

,進行了序列化,而在反序列化的時候,修改了類的結構(添加或删除成員變量,修改成員變量的命名),此時會報錯。

2)如果添加serialVersionUID,進行了序列化,而在反序列化的時候,修改了類的結構(添加或删除成員變量,修改成員變量的命名),那麼可能會恢複部分資料,或者恢複不了資料。

如果設定了serialVersionUID并且一緻,那麼可能會反序列化部分資料;如果沒有設定,那麼隻要屬性不同,那麼無法反序列化。

克隆 - Cloneable

在實際程式設計過程中,會需要建立值與已經存在的對象A一樣,但是卻與A完全獨立的一個對象,即對兩個對象做修改互不影響,這時需要用克隆來建立對象B。

使用注意事項

要實作克隆,我們需要讓将要克隆對象的類實作Cloneable接口,然後重寫clone()方法。

需要注意的是,克隆對象的clone()方法并不屬于Cloneable接口,它是Object類的一個方法。這一方法的定義如下:

可以看到他是一個

protected

方法,即Object的子類才可以使用此方法。如果在沒有重寫Object的clone()方法且沒有實作Cloneable接口的執行個體上調用clone方法,會報

CloneNotSupportedException

異常。

為什麼一定要實作一個空的Cloneable接口呢?原因在于它相當于一個标志,沒有實作該接口的類是無法調用clone()方法的。這裡的标志判斷是在native方法中進行。

我們在重寫

clone()

方法後,需要将該方法設定為

public

這裡簡單介紹一下設定為

public

的原因。我們知道,protected修飾的方法隻允許本類、或本包的類、或它的子類使用。也就是說,Object的clone()方法可以被任何對象使用,因為他是任何類的父類。如果我們在自己定義的類A中重寫的clone()方法仍然是protected的,那麼在另一個不在A所在包的類B中就無法調用A的clone(),也就無法克隆A對象了。

我們也可以通過new一個對象,然後對其各個屬性指派,也能實作該需求,但是相比clone方法要做的工作太多,是以不推薦。

深克隆和淺克隆

淺克隆

Object類中的clone()方法産生的效果是:在目前記憶體中開辟一塊和原始對象一樣的記憶體空間,然後原樣拷貝原始對象中的内容。對基本資料類型來說,這樣的操作不會有問題,但是對于非基本類型的變量(如自定義的類),儲存的僅僅是對象的引用,導緻clone後的非基本類型變量和原始對象中相應的變量指向的是同一個對象,對非基本類型的變量的操作會互相影響。

舉個栗子。

假如我們有2個類,一個是Info類,記載一個同學的資訊,一個是Person類,它包括了一個Info類記載資訊,同時還有一個id值。這時如果我們要克隆一個Person對象,那麼淺拷貝僅僅是保證id值相等,裡面的的Info引用還是指向同一個Info對象的,此時如果我們要修改新Person的Info,原來Person的Info也會受到影響。

下面來測試一下。

Info類

/**
 * Created by makersy on 2019
 */

public class Info {

    String name;
    int num;


    public Info(String name, int num) {
        this.name = name;
        this.num = num;
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Info info = (Info) o;
        return num == info.num &&
                Objects.equals(name, info.name);
    }

}

           

重寫了equals方法友善比較兩個Info對象的值是否相等。

Person類

/**
 * Created by makersy on 2019
 */

public class Person implements Cloneable {

    int id;
    Info info;

    public Person() {
    }

    public Person(int id, Info info) {
        this.id = id;
        this.info = info;
    }

	//重寫clone()方法
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return id == person.id &&
                Objects.equals(info, person.info);
    }
    

    public static void main(String[] args) throws CloneNotSupportedException {
        Info info = new Info("name", 1);

        Person p = new Person(1, info);  //p -> 原對象
        Person pc = (Person) p.clone();  //pc -> 克隆後的對象

        System.out.println( p==pc );                   //false
        System.out.println( p.equals(pc));             //true
        System.out.println( p.info.equals(pc.info) );  //true
        System.out.println(p.info == pc.info);         //true

    }
}

           

從結果我們知道,pc和p是不同的兩個對象,記憶體位址也不相同;并且pc和p内部的值是一樣的。但是info卻是記憶體位址相同的同一個對象,這就不符合我們對“兩對象操作互不幹擾”的期望。

結論:

1、克隆一個對象不會調用對象的構造方法。

2、淺克隆對對象基本資料類型的修改不會互相影響,對對象非基本資料類型的修改會互相影響,是以需要實作深克隆。

深克隆

深克隆在clone()方法中除了要克隆自身對象,還要對其非基本資料類型的成員變量克隆一遍,是以這就導緻非基本資料類型的成員變量也需要實作上述的實作clone的兩個操作。

深克隆的方式有兩種:

  1. 使用Cloneable接口

    第一步,克隆的類要實作Cloneable接口和重寫Object的clone()方法;

    第二步,先調用super.clone()方法克隆出一個新對象,然後手動給克隆出來的對象的非基本資料類型的成員變量指派。

  2. 使用序列化

    克隆對象實作Serializable接口。先對對象進行序列化,緊接着馬上反序列化。需要注意克隆對象的非基本資料類型成員也需要實作Serializable接口,否則會報錯,成員無法被序列化。

在資料結構比較複雜的情況下,序列化和反序列化可能實作起來簡單,方法1實作比較複雜。但是方法1效率會高一些,因為clone方法是native方法嘛。

下面介紹下這兩種方法。示範類還是采用上述的兩個Info和Person。

第一種方法

Info類

/**
 * Created by makersy on 2019
 */

public class Info implements Cloneable{

    String name;
    int num;


    public Info(String name, int num) {
        this.name = name;
        this.num = num;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Info info = (Info) o;
        return num == info.num &&
                Objects.equals(name, info.name);
    }
}

           

Info沒什麼大變化,隻是實作了Cloneable接口,覆寫了clone()方法。

Person類

/**
 * Created by makersy on 2019
 */

public class Person implements Cloneable {

    int id;
    Info info;

    public Person(int id, Info info) {
        this.id = id;
        this.info = info;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        Person person = (Person) super.clone();
        Info temp = (Info) person.info.clone();
        person.info = temp;
        return person;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return id == person.id &&
                Objects.equals(info, person.info);
    }


    public static void main(String[] args) throws CloneNotSupportedException {
        Info info = new Info("name", 1);


        Person p = new Person(1, info);

        Person pc = (Person) p.clone();

        System.out.println( p==pc );                   //false
        System.out.println( p.equals(pc));             //true
        System.out.println(p.info == pc.info);         //false
        System.out.println( p.info.equals(pc.info) );  //true

        pc.info.name = "張三";
        System.out.println(p.info.name);               //name
        System.out.println(pc.info.name);              //張三

    }
}

           

實作深克隆之後,對新對象pc的info進行更改,并不會影響到p的info。這就證明兩個Person對象的基本資料類型和非基本資料類型都實作了克隆。

第二種方法:序列化

Info類

/**
 * Created by makersy on 2019
 */

public class Info implements Serializable {

    private static final long serialVersionUID = 2316822637750102608L;  //最好顯式聲明序列化ID

    String name;
    int num;

    public Info(String name, int num) {
        this.name = name;
        this.num = num;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Info info = (Info) o;
        return num == info.num &&
                Objects.equals(name, info.name);
    }

}

           

Person類

/**
 * Created by makersy on 2019
 */

public class Person implements Serializable {

    private static final long serialVersionUID = 4773247170650162945L;

    int id;
    Info info;

    public Person() {
    }

    public Person(int id, Info info) {
        this.id = id;
        this.info = info;
    }

    //序列化實作深克隆,傳回深克隆對象
    public Person myClone() {
        Person person = null;
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);  //序列化對象
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            person = (Person) ois.readObject();  //反序列化對象
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return person;
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return id == person.id &&
                Objects.equals(info, person.info);
    }


    public static void main(String[] args) throws CloneNotSupportedException {
        Info info = new Info("name", 1);


        Person p = new Person(1, info);

//        Person pc = (Person) p.clone();
        Person pc = p.myClone();

        System.out.println( p==pc );
        System.out.println( p.equals(pc));
        System.out.println(p.info == pc.info);
        System.out.println( p.info.equals(pc.info) );

        pc.info.name = "張三";
        System.out.println(p.info.name);
        System.out.println(pc.info.name);

    }
}

           

沒有貼輸出結果,因為和第一種方式結果是一樣的。

總結

本篇文章記錄了為什麼要序列化機制,如何将對象序列化和反序列化,如何對對象進行深、淺克隆,以及在使用這些方式時的一些注意事項。

關于這些,還有更深層次的東西等待我去挖掘,我要繼續努力呀~