1. 程式人生 > >JAVA中clone方法詳解

JAVA中clone方法詳解

為了理解javaclone,有必要先了解一些東西。java的型別,java的型別分為兩大類,一類為primitive,如int,另一類為引用型別,String,Object等等。java引用型別的儲存,java引用型別都是儲存在堆上的 

Java程式碼

publicclass B {  

int a;  

    String b;  

public B(int a, String b) {  

super();  

this.a = a;  

this.b = b;  

    }  

}  

對這樣一個引用型別的例項,我們可以推測,在堆上它的記憶體儲存形式(除去指向class的引用,鎖的管理等等內務事務所佔記憶體),應該有一個

int值表示a,以及一個引用,該引用指向b在堆上的儲存空間。為什麼要clone恩,因為需要。廢話。 有名的GoF設計模式裡有一個模式為原型模式,用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件簡單的說就是clone一個物件例項。使得clone出來的copy和原有的物件一模一樣。插一個簡單使用clone的例子,如果一個物件內部有可變物件例項的話,public API不應該直接返回該物件的引用,以防呼叫方的code改變該物件的內部狀態。這個時候可以返回該物件的clone問題來了,什麼叫一模一樣。 一般來說,有 x.clone() != x x.clone().getClass() == x.getClass() 
x.clone().equals(x) 但是這些都不是強制的。 我們需要什麼樣的clone就搞出什麼樣的clone好了。一般而言,我們要的clone應該是這樣的。copy和原型的內容一樣,但是又是彼此隔離的。即在clone之後,改變其中一個不影響另外一個。Objectclone以及為什麼如此實現Objectclone的行為是最簡單的。以堆上的記憶體儲存解釋的話(不計內務記憶體),對一個物件aclone就是在堆上分配一個和a在堆上所佔儲存空間一樣大的一塊地方,然後把a的堆上記憶體的內容複製到這個新分配的記憶體空間上。看例子。 

Java程式碼

class User {  

    String name;  

int age;  

}  

class Account implementsCloneable {  

    User user;  

long balance;  

@Override

public Object clone()throws CloneNotSupportedException {  

returnsuper.clone();  

    }  

}  

Java程式碼

// user.

User user = new User();  

user.name = "user";  

user.age = 20;  

// account.

Account account = new Account();  

account.user = user;  

account.balance = 10000;  

// copy.

Account copy = (Account) account.clone();  

// balance因為是基本型別,所以copy和原型是相等且獨立的

Assert.assertEquals(copy.balance, account.balance);  

copy.balance = 20000;  

// 改變copy不影響原型。

Assert.assertTrue(copy.balance != account.balance);  

// user因為是引用型別,所以copy和原型的引用是同一的

Assert.assertTrue(copy.user == account.user);  

copy.user.name = "newName";  

// 改變的是同一個東西原形也跟著改變了

Assert.assertEquals("newName", account.user.name);  

恩,預設實現是幫了我們一些忙,但是不是全部。 primitive的確做到了相等且隔離。引用型別僅僅是複製了一下引用,copy和原型引用的東西是一樣的 這個就是所謂的copy了。 要實現深copy即複製原型中物件的記憶體copy而不僅僅是一個引用。只有自己動手了。 等等,是不是所有的引用型別都需要深copy呢?不是! 我們之所以要深copy,是因為預設的實現提供的淺copy不是隔離的,換言之,改變copy的東西,會影響到原型的內部。比如例子中,改變copyusername,影響了原型。如果我們要copy的類是不可變的呢,如String,沒有方法可以改變它的內部狀態呢。

Java程式碼

class User implements Cloneable {  

    String name;  

int age;  

@Override

public Object clone()throws CloneNotSupportedException {  

returnsuper.clone();  

    }  

}  

Java程式碼

// user.

User user = new User();  

user.name = "user";  

user.age = 20;  

// copy

User copy = (User) user.clone();  

// age因為是primitive,所以copy和原型是相等且獨立的。

Assert.assertEquals(copy.age, user.age);  

copy.age = 30;  

// 改變copy不影響原型。

Assert.assertTrue(copy.age != user.age);  

// name因為是引用型別,所以copy和原型的引用是同一的

Assert.assertTrue(copy.name == user.name);  

// String為不可變類。沒有辦法可以通過對copy.name的字串的操作改變這個字串。

// 改變引用新的物件不會影響原型。

copy.name = "newname";  

Assert.assertEquals("newname", copy.name);  

Assert.assertEquals("user", user.name);  

可見,在考慮clone時,primitive不可變物件型別是可以同等對待的都不會影響原形 java為什麼如此實現clone呢?也許有以下考慮。 1 效率和簡單性,簡單的copy一個物件在堆上的的記憶體比遍歷一個物件網然後記憶體深copy明顯效率高並且簡單。2 不給別的類強加意義。如果A(例一中的account實現了Cloneable同時有一個引用指向B(例一中的user,如果直接複製記憶體進行深copy的話,意味著B在意義上也是支援Clone,但是這個是在使用BA中做的,B甚至都不知道。破壞了B原有的介面。3 有可能破壞語義。如果A實現了Cloneable,同時有一個引用指向B,該B實現為單例模式,如果直接複製記憶體進行深copy的話,破壞了B的單例模式。4 方便且更靈活,如果A引用一個不可變物件,則記憶體deep copy是一種浪費。Shadow copy給了程式設計師更好的靈活性。如何cloneclone三部曲。1 宣告實現Cloneable介面。2 呼叫super.clone拿到一個物件,如果父類的clone實現沒有問題的話,在該物件的記憶體儲存中,所有父類定義的field都已經clone好了,該類中的primitive和不可變型別引用也克隆好了,可變型別引用都是淺copy3 把淺copy的引用指向原型物件新的克隆體。給個例子。 

Java程式碼

class User implements Cloneable {  

    String name;  

int age;  

@Override

public User clone()throws CloneNotSupportedException {  

return (User) super.clone();  

    }  

}  

class Account implements Cloneable {  

    User user;  

long balance;  

@Override

public Account clone()throws CloneNotSupportedException {  

       Account account = null;  

        account = (Account) super.clone();  

if (user != null) { //分離了對user的引用

           account.user = user.clone();  

        }  

return account;  

    }  

}  

clone的態度clone嘛,我覺得是個好東西,畢竟系統預設實現已經幫我們做了很多事情了。但是它也是有缺點的。 1 手工維護clone的呼叫鏈。這個問題不大,程式設計師有責任做好。2 如果classfield是個final的可變類,就不行了。三部曲的第三步沒有辦法做了。考慮一個類對clone的態度,有如下幾種。1 公開支援:好吧,按照clone三部曲實現吧。前提是父類支援(公開或者默默)。2 默默支援:不實現Cloneable介面,但是在類裡面有正確的protectedclone實現,這樣,該類不支援clone,但是它的子類如果想支援的話也不妨礙。3 不支援:好吧,為了明確該目的,提供一個拋CloneNotSupportedException異常的protectedclone實現。4 看情況支援:該類內部可以儲存其他類的例項,如果其他類支援則該類支援,如果其他類不支援,該類沒有辦法,只有不支援。其他的選擇可以用原型建構函式,或者靜態copy方法來手工製作一個物件的copy好處是即使classfieldfinal,也不會影響該方法的使用。不好的地方是所有的primitive賦值都得自己維護。Serializable的比較使用Serializable同樣可以做到物件的clone。但是:Cloneable本身就是為clone設計的,雖然有一些缺點,但是如果它可以clone的話無疑用它來做clone比較合適。如果不行的話用原型建構函式,或者靜態copy方法也可以。Serializable製作clone的話,添加了太多其它的東西,增加了複雜性。1 所有的相關的類都得支援Serializable。這個相比支援Cloneable只會工作量更大2 Serializable添加了更多的意義,除了提供一個方法用Serializable製作Clone,該類等於也添加了其它的public API,如果一個類實現了Serializable,等於它的2進位制形式就已經是其API的一部分了,不便於該類以後內部的改動。3 當類用Serializable來實現clone時,使用者如果儲存了一個老版本的物件2進位制,該類升級,使用者用新版本的類反系列化該物件,再呼叫該物件用Serializable實現的clone。這裡為了一個clone的方法又引入了類版本相容性的問題。不划算。效能不可否認,JVM越來越快了。但是系統預設的native實現還是挺快的。clone一個有100個元素的int陣列,用系統預設的clone比靜態copy方法快2倍左右。

1.淺複製與深複製概念
淺複製(淺克隆)
被複制物件的所有變數都含有與原來的物件相同的值,而所有的對其他物件的引用仍然指向 原來的物件。換言之,淺複製僅僅複製所考慮的物件,而不復制它所引用的物件。

深複製(深克隆)
被複制物件的所有變數都含有與原來的物件相同的值,除去那些引用其他物件的變數。那些 引用其他物件的變數將指向被複制過的新物件,而不再是原有的那些被引用的物件。換言之,深複製把要複製的物件所引用的物件都複製了一遍。

2Javaclone()方法
⑴clone方法將物件複製了一份並返回給呼叫者。一般而言,clone()方法滿足:
對任何的物件x,都有x.clone() !=x//克隆物件與原物件不是同一個物件
對任何的物件x,都有x.clone().getClass()= =x.getClass()//克隆物件與原物件的型別一樣
如果物件xequals()方法定義恰當,那麼x.clone().equals(x)應該成立。

⑵Java中物件的克隆
為了獲取物件的一份拷貝,我們可以利用Object類的clone()方法。
在派生類中覆蓋基類的clone()方法,並宣告為public
在派生類的clone()方法中,呼叫super.clone()
在派生類中實現Cloneable介面。

請看如下程式碼:

class Student implements Cloneable
{
    String name;
    int age;
    Student(String name,int age)
    {
        this.name=name;
        this.age=age;
    }
    public Object clone()
    {
        Object o=null;
        try
        {
        o=(Student)super.clone();//Object中的clone()識別出你要複製的是哪一個物件。
        }
        catch(CloneNotSupportedException e)
        {
            System.out.println(e.toString());
        }
        return o;
    }
}

public static void main(String[] args)
    {
      Student s1=new Student("zhangsan",18);
      Student s2=(Student)s1.clone();
      s2.name="lisi";
     s2.age=20;
System.out.println("name="+s1.name+","+"age="+s1.age); //修改學生2後,不影響學生1的值。
   }

說明:
為什麼我們在派生類中覆蓋Objectclone()方法時,一定要呼叫super.clone()呢?在執行時 刻,Object中的clone()識別出你要複製的是哪一個物件,然後為此物件分配空間,並進行物件的複製,將原始物件的內容一一複製到新物件的存 儲空間中。
繼承自java.lang.Object類的clone()方法是淺複製。以下程式碼可以證明之。

class Professor
{
    String name;
    int age;
    Professor(String name,int age)
    {
        this.name=name;
        this.age=age;
    }
}
class Student implements Cloneable
{
    String name;//常量物件。
    int age;
    Professor p;//學生1和學生2的引用值都是一樣的。
    Student(String name,int age,Professor p)
    {
        this.name=name;
        this.age=age;
        this.p=p;
    }
    public Object clone()
    {
        Student o=null;
        try
        {
            o=(Student)super.clone();
        }
        catch(CloneNotSupportedException e)
        {
            System.out.println(e.toString());
        }
        o.p=(Professor)p.clone();
        return o;
    }
}
public static void main(String[] args)
    {
      Professor p=new Professor("wangwu",50);
      Student s1=new Student("zhangsan",18,p);
      Student s2=(Student)s1.clone();
      s2.p.name="lisi";
     s2.p.age=30;
System.out.println("name="+s1.p.name+","+"age="+s1.p.age); //學生1的教授成為lisi,age30
}

那應該如何實現深層次的克隆,即修改s2的教授不會影響s1的教授?程式碼改進如下。

改進使學生1Professor不改變(深層次的克隆)
class Professor implements Cloneable
{
    String name;
    int age;
    Professor(String name,int age)
    {
        this.name=name;
        this.age=age;
    }
    public Object clone()
    {
        Object o=null;
        try
        {
            o=super.clone();
        }
        catch(CloneNotSupportedException e)
        {
            System.out.println(e.toString());
        }
        return o;
    }
}
class Student implements Cloneable
{
    String name;
    int age;
    Professor p;
    Student(String name,int age,Professor p)
    {
        this.name=name;
        this.age=age;
        this.p=p;
    }
    public Object clone()
    {
        Student o=null;
        try
        {
            o=(Student)super.clone();
        }
        catch(CloneNotSupportedException e)
        {
            System.out.println(e.toString());
        }
        o.p=(Professor)p.clone();
        return o;
    }
}
public static void main(String[] args)
    {
      Professor p=new Professor("wangwu",50);
      Student s1=new Student("zhangsan",18,p);
      Student s2=(Student)s1.clone();
      s2.p.name="lisi";
     s2.p.age=30;
System.out.println("name="+s1.p.name+","+"age="+s1.p.age); //學生1的教授不改變。
}

3.利用序列化來做深複製
把物件寫到流裡的過程是序列化(Serilization)過程,但是在Java程式 師圈子裡又非常形象地稱為冷凍或者醃鹹菜(picking過程;而把物件從流中讀出來的並行化(Deserialization)過程則叫做解凍或者回鮮(depicking)”過程。應當指出的是,寫在流裡的是物件的一個拷貝,而原物件仍然存在於JVM裡面,因此醃成鹹菜的只是對 象的一個拷貝,Java鹹菜還可以回鮮。
Java語言裡深複製一個物件,常常可以先使物件實現Serializable接 口,然後把物件(實際上只是物件的一個拷貝)寫到一個流裡(醃成鹹菜),再從流裡讀出來(把鹹菜回鮮),便可以重建物件。
如下為深複製原始碼。
public Object deepClone()
{
//將物件寫到流裡
ByteArrayOutoutStream bo=new ByteArrayOutputStream();
ObjectOutputStream oo=new ObjectOutputStream(bo);
oo.writeObject(this);
//從流裡讀出來
ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi=new ObjectInputStream(bi);
return(oi.readObject());
}

這樣做的前提是物件以及物件內部所有引用到的物件都是可序列化的,否則,就需要仔細考 察那些不可序列化的物件可否設成transient,從而將之排除在複製過程之外。上例程式碼改進如下。

class Professor implements Serializable
{
    String name;
    int age;
    Professor(String name,int age)
    {
        this.name=name;
        this.age=age;
    }
}
class Student implements Serializable
{
    String name;//常量物件。
    int age;
    Professor p;//學生1和學生2的引用值都是一樣的。
    Student(String name,int age,Professor p)
    {
        this.name=name;
        this.age=age;
        this.p=p;
    }
    public Object deepClone() throws IOException,
OptionalDataException,ClassNotFoundException
{
//將物件寫到流裡
ByteArrayOutoutStream bo=new ByteArrayOutputStream();
ObjectOutputStream oo=new ObjectOutputStream(bo);
oo.writeObject(this);
//從流裡讀出來
ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi=new ObjectInputStream(bi);
return(oi.readObject());
}

}
public static void main(String[] args)
    {
      Professor p=new Professor("wangwu",50);
      Student s1=new Student("zhangsan",18,p);
      Student s2=(Student)s1.deepClone();
      s2.p.name="lisi";
     s2.p.age=30;
System.out.println("name="+s1.p.name+","+"age="+s1.p.age); //學生1的教授不改變。
}