Java基礎22:Cloneable和Serializable接口

一、Cloneable接口

在實際編程過程中,我們常常要遇到這種情況:有一個對象A,在某一時刻A中已經包含了一些有效值,此時可能會需要一個和A完全相同的新對象B,並且此後對B任何改動都不會影響到A中的值。也就是說,A與B是兩個獨立的對象,但B的初始值是由A對象確定的。在Java語言中,用簡單的賦值語句是不能滿足這種需求的,需要使用clone。

clone:它允許在堆中克隆出一塊和原對象一樣的對象,並將這個對象的地址賦予新的引用。簡而言之,克隆就是快速構造一個和已有對象相同的副本。

Java中的類要實現clone功能必須實現java.lang.Cloneable接口。Cloneable接口屬於合法標誌性接口(接口內不含有任何方法),只有實現這個接口後,然後在類中重寫Object中的clone()方法,然後通過類的實例對象調用clone()方法才能克隆成功,否則就會拋出CloneNotSupportedException異常。

Java中所有類都默認繼承java.lang.Object類,在java.lang.Object類中有一個方法clone(),這個方法將返回Object對象的一個拷貝。要說明的有兩點:一是拷貝對象返回的是一個新對象,而不是一個引用;二是拷貝對象與用 new操作符返回的新對象的區別就是這個拷貝已經包含了一些原來對象的信息,而不是對象的初始信息。

如果一個類重寫了 Object 內定義的 clone()方法 ,需要同時實現 Cloneable 接口(雖然這個接口內並沒有定義 clone() 方法),否則會拋出異常,也就是說, Cloneable接口只是個合法調用 clone() 的標識(marker-interface)。

class CloneClass implements Cloneable
{
  public int aInt;

   //重寫Object中的clone方法,並聲明爲public
  public Object clone()
  {
   CloneClass o = null;
   try
    {
     o = (CloneClass)super.clone();//調用父類Object的clone()方法
   }
     catch(CloneNotSupportedException e)
    {
      e.printStackTrace();
   }
   return o;
 }
}

1、有三個值得注意的地方:

a>爲了實現clone功能,CloneClass類實現了Cloneable接口,這個接口屬於java.lang 包,java.lang包已經被缺省的導入類中,所以不需要寫成java.lang.Cloneable;

b>重寫java.lang.Object.clone()方法,Object類中的clone()方法是一個protected屬性的方法。這也意味着如果要應用clone()方法,必須繼承Object類,在 Java中所有的類是缺省繼承Object類的,也就不用關心這點了。

這裏有一個疑問,Object中的clone方法是一個空的方法,那麼他是如何判斷類是否實現了cloneable接口呢?

原因在於這個方法中有一個native關鍵字修飾。native修飾的方法都是空的方法,但是這些方法都是有實現體的(這裏也就間接說明了native關鍵字不能與abstract同時使用。因爲abstract修飾的方法與java的接口中的方法類似,他顯式的說明了修飾的方法,在當前是沒有實現體的,abstract的方法的實現體都由子類重寫),只不過native方法調用的實現體,都是非java代碼編寫的(調用的是在jvm中編寫的C的接口),每一個native方法在jvm中都有一個同名的實現體,native方法在邏輯上的判斷都是由實現體實現的,另外這種native修飾的方法對返回類型,異常控制等都沒有約束。

 由此可見,這裏判斷是否實現cloneable接口,是在調用jvm中的實現體時進行判斷的。

總結:Object類的clone()方法是一個native方法,native方法的效率一般來說都是遠高於java中的非native方法。這也解釋了爲 什麼要用Object中clone()方法而不是先new一個對象,然後把原始對象中的信息賦到新對象中,雖然這也實現了clone功能,但因爲這個實例的創建過程十分複雜,在執行過程中會消耗大量的時間,所以導致效率較低。

c>爲了讓其它類能調用這個clone 類的clone()方法,重載之後要把clone()方法的屬性設置爲public。

2、深入理解深度克隆與淺度克隆

首先,在Java中創建對象的方式有四種:

        一種是new,通過new關鍵字在堆中爲對象開闢空間,在執行new時,首先會看所要創建的對象的類型,知道了類型,才能知道需 要給這個對象分配多大的內存區域,分配內存後,調用對象的構造函數,填充對象中各個變量的值,將對象初始化,然後通過構造方法返回對象的地址;

      另一種是clone,clone也是首先分配內存,這裏分配的內存與調用clone方法對象的內存相同,然後將源對象中各個變量的值,填充到新的對象中,填充完成後,clone方法返回一個新的地址,這個新地址的對象與源對象相同,只是地址不同。

另外還有輸入輸出流,反射構造對象等

深度克隆和淺度克隆,這東西雖然平常不怎麼用,但是瞭解一下還是有必要的。Object中的克隆方法是淺度克隆,JDK規定了克隆需要滿足的一些條件,簡要總結一下就是:對某個對象進行克隆,對象的的成員變量如果包括引用類型或者數組,那麼克隆的時候其實是不會把這些對象也帶着複製到克隆出來的對象裏面的,只是複製一個引用,這個引用指向被克隆對象的成員對象,但是基本數據類型是會跟着被帶到克隆對象裏面去的。而深度可能就是把對象的所有屬性都統統複製一份新的到目標對象裏面去。

如下圖所示:

 

Java實現深度克隆的簡單方法使用Java的流,先將對象序列化,然後序列化回對象,其中的限制爲克隆的對象必須實現Serializable接口.。

Java實現深度克隆的簡單方法

 

二、Serializable接口

Serializable接口中一個成員函數或者成員變量也沒有,這個接口的作用就是實現序列化,那什麼是序列化?

1、序列化

Java提供了一種保存對象狀態的機制,那就是序列化。

對象的壽命通常隨着生成該對象的程序的終止而終止,而有時候需要把在內存中的各種對象的狀態(也就是實例變量,不是方法)保存下來,並且可以在需要時再將對象恢復。

Java 序列化技術可以將一個對象的狀態寫入一個Byte 流裏(序列化),並且可以從其它地方把該Byte 流裏的數據讀出來(反序列化)。

2、什麼時候需要序列化

想把內存中的對象狀態保存到一個文件中或者數據庫中時候;
想把對象通過網絡進行傳播的時候。

3、如何序列化

只要一個類實現Serializable接口,那麼這個類就可以序列化了。

舉個栗子:

class Person implements Serializable
{   
    //一會就說這個是做什麼的
    private static final long serialVersionUID = 1L; 

    String name;
    int age;
    public Person(String name,int age){
        this.name = name;
        this.age = age;
    }   
    public String toString(){
        return "name:"+name+"\tage:"+age;
    }
}

通過ObjectOutputStream 的writeObject()方法把這個類的對象寫到一個地方(文件),再通過ObjectInputStream 的readObject()方法把這個對象讀出來。

File file = new File("file"+File.separator+"out.txt");

    /*
     * 1、序列化
     */
    FileOutputStream fos = null;
    try {
        fos = new FileOutputStream(file);
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(fos);
            Person person = new Person("tom", 22);
            // 調用 person的 tostring() 方法
            System.out.println(person);
            //寫入對象
            oos.writeObject(person);            
            oos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            try {
                oos.close();
            } catch (IOException e) {
                System.out.println("oos關閉失敗:"+e.getMessage());
            }
        }
    } catch (FileNotFoundException e) {
        System.out.println("找不到文件:"+e.getMessage());
    } finally{
        try {
            fos.close();
        } catch (IOException e) {
            System.out.println("fos關閉失敗:"+e.getMessage());
        }
    }

   /*
     *2、反序列化
     */
    FileInputStream fis = null;
    try {
        fis = new FileInputStream(file);
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(fis);
            try {
                Person person = (Person)ois.readObject();   //讀出對象
                System.out.println(person);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } 
        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            try {
                ois.close();
            } catch (IOException e) {
                System.out.println("ois關閉失敗:"+e.getMessage());
            }
        }
    } catch (FileNotFoundException e) {
        System.out.println("找不到文件:"+e.getMessage());
    } finally{
        try {
            fis.close();
        } catch (IOException e) {
            System.out.println("fis關閉失敗:"+e.getMessage());
        }
    }

運行結果:

name:tom    age:22
name:tom    age:22

結果完全一樣;如果把Person類中的implements Serializable 去掉,Person類就不能序列化了,此時再運行上述程序,就會報java.io.NotSerializableException異常。

4、serialVersionUID

注意到上面程序中有一個 serialVersionUID ,實現了Serializable接口之後,Eclipse就會提示你增加一個 serialVersionUID,雖然不加的話上述程序依然能夠正常運行。

序列化 ID 在 Eclipse 下提供了兩種生成策略

一個是固定的 1L
一個是隨機生成一個不重複的 long 類型數據(實際上是使用 JDK 工具,根據類名、接口名、成員方法及屬性等來生成)
上面程序中,輸出對象和讀入對象使用的是同一個Person類。

如果是通過網絡傳輸的話,如果Person類的serialVersionUID不一致,那麼反序列化就不能正常進行。例如在客戶端A中Person類的serialVersionUID=1L,而在客戶端B中Person類的serialVersionUID=2L, 那麼就不能重構這個Person對象。

試圖重構就會報java.io.InvalidClassException異常,因爲這兩個類的版本不一致,local class incompatible,重構就會出現錯誤。如果沒有特殊需求的話,使用用默認的 1L 就可以,這樣可以確保代碼一致時反序列化成功。那麼隨機生成的序列化 ID 有什麼作用呢,有些時候,通過改變序列化 ID 可以用來限制某些用戶的使用。

5、transient關鍵字

經常在實現了 Serializable接口的類中能看見transient關鍵字。 transient關鍵字的作用是:阻止實例中那些用此關鍵字聲明的變量持久化;當對象被反序列化時(從源文件讀取字節序列進行重構),這樣的實例變量值不會被持久化和恢復。

當某些變量不想被序列化,同是又不適合使用static關鍵字聲明,那麼此時就需要用transient關鍵字來聲明該變量。

例如用 transient關鍵字 修飾name變量

class Person implements Serializable{   

    private static final long serialVersionUID = 1L;

    transient String name;
    int age;

    public Person(String name,int age){
        this.name = name;
        this.age = age;
    }   
    public String toString(){
        return "name:"+name+"\tage:"+age;
    }
}

在反序列化視圖重構對象的時候,作用與static變量一樣, 輸出結果爲:

name:null   age:22

在被反序列化後,transient 變量的值被設爲初始值,如 int 型的是 0,對象型的是 null。

注:對於某些類型的屬性,其狀態是瞬時的,這樣的屬性是無法保存其狀態的。例如一個線程屬性或需要訪問IO、本地資源、網絡資源等的屬性,對於這些字段,我們必須用transient關鍵字標明,否則編譯器將報措。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章