1. 程式人生 > >Java中 Cloneable 、Serializable 介面詳解

Java中 Cloneable 、Serializable 介面詳解

Cloneable介面

clone:它允許在堆中克隆出一塊和原物件一樣的物件,並將這個物件的地址賦予新的引用。
Java 中 一個類要實現clone功能 必須實現 Cloneable介面,否則在呼叫 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;
 public Object clone(){
  CloneClass o = null;
  try{
   o = (CloneClass)super.clone();
  }catch
(CloneNotSupportedException e){    e.printStackTrace();   }   return o;  } }

有三個值得注意的地方:
一是為了實現clone功能,CloneClass類實現了Cloneable介面,這個介面屬於java.lang 包,java.lang包已經被預設的匯入類中,所以不需要寫成java.lang.Cloneable;
二是過載了clone()方 法;
三是在clone()方法中呼叫了super.clone(),這也意味著無論clone類的繼承結構是什麼樣的,super.clone()直接或 間接呼叫了java.lang.Object類的clone()方法。

Object類的clone()方法是一個native方法,native方法的效率一般來說都是遠高於java中的非native方法。這也解釋了為 什麼要用Object中clone()方法而不是先new一個物件,然後把原始物件中的資訊賦到新物件中,雖然這也實現了clone功能,但效率較低。

Object類中的clone()方法還是一個protected屬性的方法。這也意味著如果要應用clone()方法,必須繼承Object類,在 Java中所有的類是預設繼承Object類的,也就不用關心這點了。

然後過載clone()方法。還有一點要考慮的是為了讓其它類能呼叫這個clone 類的clone()方法,過載之後要把clone()方法的屬性設定為public。
參考

Serializable介面

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

序列化

序列化:物件的壽命通常隨著生成該物件的程式的終止而終止,而有時候需要把在記憶體中的各種物件的狀態(也就是例項變數,不是方法)儲存下來,並且可以在需要時再將物件恢復。 Java提供了一種儲存物件狀態的機制,那就是序列化。

Java 序列化技術可以將一個物件的狀態寫入一個Byte 流裡(序列化),並且可以從其它地方把該Byte 流裡的資料讀出來(反序列化)。

什麼時候需要序列化

想把記憶體中的物件狀態儲存到一個檔案中或者資料庫中時候;
想把物件通過網路進行傳播的時候

如何序列化

只要一個類實現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");

    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());
        }
    }

    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異常。

serialVersionUID

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

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

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

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

客戶端A中的Person類:

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;
    }
}

客戶端B中的Person類:

class Person implements Serializable{   

    private static final long serialVersionUID = 2L;

    String name;
    int age;

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

試圖重構就會報java.io.InvalidClassException異常,因為這兩個類的版本不一致,local class incompatible,重構就會出現錯誤。

如果沒有特殊需求的話,使用用預設的 1L 就可以,這樣可以確保程式碼一致時反序列化成功。那麼隨機生成的序列化 ID 有什麼作用呢,有些時候,通過改變序列化 ID 可以用來限制某些使用者的使用。

靜態變數序列化
序列化只能儲存物件的非靜態成員交量,不能儲存任何的成員方法和靜態的成員變數,而且序列化儲存的只是變數的值,對於變數的任何修飾符都不能儲存。

如果把Person類中的name定義為static型別的話,試圖重構,就不能得到原來的值,只能得到null。說明對靜態成員變數值是不儲存的。這其實比較容易理解,序列化儲存的是物件的狀態,靜態變數屬於類的狀態,因此 序列化並不儲存靜態變數。

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關鍵字標明,否則編譯器將報措。

序列化中的繼承問題

當一個父類實現序列化,子類自動實現序列化,不需要顯式實現Serializable介面;
一個子類實現了 Serializable 介面,它的父類都沒有實現 Serializable 介面,要想將父類物件也序列化,就需要讓父類也實現Serializable 介面。
第二種情況中:如果父類不實現 Serializable介面的話,就需要有預設的無參的建構函式。這是因為建立java 物件的時候需要先有父物件,才有子物件,反序列化也不例外。在反序列化時,為了構造父物件,只能呼叫父類的無參建構函式作為預設的父物件。因此當我們取父物件的變數值時,它的值是呼叫父類無參建構函式後的值。在這種情況下,在序列化時根據需要在父類無參建構函式中對變數進行初始化,否則的話,父類變數值都是預設宣告的值,如 int 型的預設是 0,string 型的預設是 null。

例項

class People{
    int num;
    public People(){}           //預設的無參建構函式,沒有進行初始化
    public People(int num){     //有參建構函式
        this.num = num;
    }
    public String toString(){
        return "num:"+num;
    }
}
class Person extends People implements Serializable{    

    private static final long serialVersionUID = 1L;

    String name;
    int age;

    public Person(int num,String name,int age){
        super(num);             //呼叫父類中的建構函式
        this.name = name;
        this.age = age;
    }
    public String toString(){
        return super.toString()+"\tname:"+name+"\tage:"+age;
    }
}

將物件寫到檔案中

//呼叫帶引數的建構函式num=10,name = "tim",age =22
Person person = new Person(10,"tom", 22); 
    System.out.println(person);
    oos.writeObject(person);    

從檔案中讀出物件

//反序列化,呼叫父類中的無參構函式
Person person = (Person)ois.readObject(); 
    System.out.println(person);

輸出

num:0   name:tom    age:22

參考