什么是clone
在實際編程過程中,我們常常要遇到這種情況:有一個對象object1,在某一時刻object1中已經包含了一些有效值,此時可能會需要一個和object1完全相同新對象object2,並且此后對object2任何改動都不會影響到object1中的值,也就是說,object1與object2是兩個獨立的對象,但object2的初始值是由object1對象確定的。在Java語言中,用簡單的賦值語句是不能滿足這種需 求的。要滿足這種需求雖然有很多途徑,但實現clone()方法是其中最簡單,也是最高效的手段。
Java的所有類都默認繼承java.lang.Object類,在java.lang.Object類中有一個方法clone(),該方法在Object中的定義如下:
/** * Class Object is the root of the class hierarchy. Every class has Object as a superclass. * All objects, including arrays, implement the methods of this class. */ public class Object { /** * Creates and returns a copy of this object. The precise meaning of "copy" may depend on the class of the object. * The general intent is that, for any object {@code x}, the expression: x.clone() != x will be true, * and that the expression: x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements. * While it is typically the case that: x.clone().equals(x) will be true, this is not an absolute requirement. */ protected native Object clone() throws CloneNotSupportedException; }
從上面對clone方法的注解可知clone方法的通用約定:對於任意一個對象x,表達式①x.clone != x將會是true;表達式②x.clone().getClass()==x.getClass()將會是true,但不是絕對的;表達式③x.clone().equals(x)將會是true,但是這也不是絕對的。
從源代碼可知,根類Object的clone方法是用protected關鍵字修飾,這樣做是為避免我們創建每一個類都默認具有克隆能力。
如何使用clone方法
要使類具有克隆能力能力時,需要實現Cloneable接口,實現它的目的是作為一個對象的一個mixin(混入)接口,表明這個對象是允許克隆的。它的源碼如下:
/** * A class implements the Cloneable interface to indicate to the {@link java.lang.Object#clone()} method that * it is legal for that method to make a field-for-field copy of instances of that class. */ public interface Cloneable { }
可以看出Cloneable是一個空接口(標記接口),它決定了Object中受保護的clone方法的實現行為:如果一個類實現了Cloneable接口,Object的clone方法就返回這個對象的逐域拷貝,否則就拋出CloneNotSupportedException異常。如果實現了這個接口,類和它所有的超類都無需調用構造器就可以創建對象。下面通過一個簡單的實例來演示clone方法的使用。
編寫一個被克隆對象Student類:
package com.kevin.clone; /** * 創建一個簡單實例演示clone方法 * @author Kevin * */ public class Student implements Cloneable { private String name; private String gender; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } @Override protected Student clone() throws CloneNotSupportedException { return (Student)super.clone(); } @Override public String toString() { return " [name=" + name + ", gender=" + gender + "]"; } }
編寫測試代碼:
package com.kevin.clone; /** * 測試clone方法 * @author Kevin * */ public class test_clone { public static void main(String[] args){ Student student1 = new Student(); student1.setName("Kevin"); student1.setGender("Male"); System.out.println("student1"+student1); try{ Student student2 = student1.clone(); System.out.println("Clone student2 from student1..."); System.out.println("student2"+student2);
System.out.println(student1.equals(student2)); System.out.println("Alter student2..."); student2.setName("Alice"); student2.setGender("Female"); System.out.println("student1"+student1); System.out.println("student2"+student2); }catch(CloneNotSupportedException e){ e.printStackTrace(); } } }
輸出結果:
student1 [name=Kevin, gender=Male] Clone student2 from student1... student2 [name=Kevin, gender=Male]
false Alter student2... student1 [name=Kevin, gender=Male] student2 [name=Alice, gender=Female]
分析:
● 一是希望能實現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。
那么clone類為什么還要實現Cloneable接口呢?需要注意的是,Cloneable接口是不包含任何方法的,其實這個接口僅僅是一個標志,而且 這個標志也僅僅是針對Object類中clone()方法的,如果clone類沒有實現Cloneable接口,並調用了Object的clone()方 法(也就是調用了super.Clone()方法),那么Object的clone()方法就會拋出 CloneNotSupportedException異常。
影子克隆和深度克隆
下面通過一個實例來演示什么是影子克隆。
編寫一個Teacher類(其中包含一個Course屬性):
package com.kevin.clone; /** * 測試影子克隆方法 * @author Kevin * */ public class Teacher implements Cloneable{ private String name; private Integer age; private Course course; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Course getCourse() { return course; } public void setCourse(Course course) { this.course = course; } @Override protected Teacher clone() throws CloneNotSupportedException { return (Teacher) super.clone(); } @Override public String toString() { return "Teacher [name=" + name + ", age=" + age + ", course=" + course + "]"; } }
package com.kevin.clone; public class Course { private String name; private Integer id; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } @Override public String toString() { return "Course [name=" + name + ", id=" + id + "]"; } }
編寫一個測試類:
package com.kevin.clone; public class Course { private String name; private Integer id; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } @Override public String toString() { return "Course [name=" + name + ", id=" + id + "]"; } }
輸出結果如下:
teacher1 [name=Kevin, age=22, course=Course [name=Math, id=66]] Clone teacher2 from teacher1... teacher2 [name=Kevin, age=22, course=Course [name=Math, id=66]] Alter teacher2... teacher1 [name=Kevin, age=22, course=Course [name=English, id=88]] teacher2 [name=Ryan, age=18, course=Course [name=English, id=88]]
通過分析結果可知,當我們修改克隆對象teacher2的時候,teacher1的course屬性也被修改了,如果通過查看內存地址的形式我們可以發現,他們兩個course其實是同一個對象。由此我們可以推斷,調用clone方法產生的效果是:現在內存中開辟一塊和原始對象一樣的空間,然后拷貝原始對象中的內容。但是對於基本數據類型,這樣的操作是沒有問題的,但對於非基本類型,它們保存的僅僅是對象的引用,這就是為什么clone后的非基本類型變量和原始對象中相應的變量會指向的是同一個對象。這就是所謂的影子克隆。
為了解決影子克隆所產生的問題,我們就需要使用深度克隆方案。通過對以上實例改進后的方案如下:
package com.kevin.clone; /** * 測試影子克隆方法 * @author Kevin * */ public class Teacher implements Cloneable{ private String name; private Integer age; private Course course; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Course getCourse() { return course; } public void setCourse(Course course) { this.course = course; } @Override protected Teacher clone() throws CloneNotSupportedException { Teacher teacher = (Teacher)super.clone(); teacher.course = course.clone(); return teacher; } @Override public String toString() { return " [name=" + name + ", age=" + age + ", course=" + course + "]"; } }
package com.kevin.clone; public class Course implements Cloneable{ private String name; private Integer id; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } protected Course clone() throws CloneNotSupportedException{ return (Course)super.clone(); } @Override public String toString() { return "Course [name=" + name + ", id=" + id + "]"; } }
同樣使用原來的測試類:
package com.kevin.clone; /** * 測試clone方法 * @author Kevin * */ public class test_clone2 { public static void main(String[] args){ Teacher t1 = new Teacher(); t1.setName("Kevin"); t1.setAge(22); Course c1 = new Course(); c1.setName("Math"); c1.setId(66); t1.setCourse(c1); System.out.println("teacher1"+t1); try{ Teacher t2 = t1.clone(); System.out.println("Clone teacher2 from teacher1..."); System.out.println("teacher2"+t2); System.out.println("Alter teacher2..."); t2.setName("Ryan"); t2.setAge(18); //修改courese屬性 t2.getCourse().setName("English"); t2.getCourse().setId(88); System.out.println("teacher1"+t1); System.out.println("teacher2"+t2); }catch(CloneNotSupportedException e){ e.printStackTrace(); } } }
輸出結果:
teacher1 [name=Kevin, age=22, course=Course [name=Math, id=66]] Clone teacher2 from teacher1... teacher2 [name=Kevin, age=22, course=Course [name=Math, id=66]] Alter teacher2... teacher1 [name=Kevin, age=22, course=Course [name=Math, id=66]] teacher2 [name=Ryan, age=18, course=Course [name=English, id=88]]
分析:由以上運行結果可知,進行過深度克隆之后,對clone產生的teacher2對象的course屬性進行修改時,並未影響到原對象teacher1的course屬性。
任何類都可以實現深度clone嗎
答案是否定的,例如,StringBuffer,看一下 JDK API中關於StringBuffer的說明,StringBuffer沒有重載clone()方法,更為嚴重的是StringBuffer還是一個 final類,這也是說我們也不能用繼承的辦法間接實現StringBuffer的clone。如果一個類中包含有StringBuffer類型對象或和 StringBuffer相似類的對象,我們有兩種選擇:要么只能實現影子clone,要么自己重新生成對象: new StringBuffer(oldValue.toString()); 進行賦值。
要知道除了基本數據類型(byte,short,int,long,double等)可自動實現深度克隆以外,其它例如Integer、String、Double等是一特殊情況。
下面通過一個實例來演示上述結論。
package com.kevin.clone; public class Book implements Cloneable{ public String name; public StringBuffer author; protected Book clone() throws CloneNotSupportedException{ return (Book)super.clone(); } }
測試代碼:
package com.kevin.clone; /** * 測試clone方法 * @author Kevin * */ public class test_clone3 { public static void main(String[] args){ Book book = new Book(); book.name = new String("Think in Java"); book.author = new StringBuffer("Kevin"); System.out.println("Before clone book.name :"+book.name); System.out.println("Before clone book.author :"+book.author); Book book_clone = null; try{ book_clone = (Book)book.clone(); }catch(CloneNotSupportedException e){ e.printStackTrace(); } book_clone.name = book_clone.name.substring(0,5); book_clone.author = book_clone.author.append(" Zhang"); System.out.println("\nAfter clone book.name :"+book.name); System.out.println("After clone book.author :"+book.author); System.out.println("\nAfter clone book_clone.name :"+book_clone.name); System.out.println("After clone book_clone.author :"+book_clone.author); } }
輸出結果:
Before clone book.name :Think in Java
Before clone book.author :Kevin
After clone book.name :Think in Java
After clone book.author :Kevin Zhang
After clone book_clone.name :Think
After clone book_clone.author :Kevin Zhang
分析:有上述結果可知,String類型的變量看起來好像實現了深度clone,因為對book_clone.name的改動並沒有影響到book.name。實質上,在clone的時候book_clone.name與book.name仍然是引用,而且都指向了同一個 String對象。但在執行book_clone.name = book_clone.name.substring(0,5)的時候,生成了一個新的String類型,然后又賦回給book_clone.name。這是因為String被 Sun公司的工程師寫成了一個不可更改的類(immutable class),在所有String類中的函數都不能更改自身的值。類似的,String類中的其它方法也是如此,都是生成一個新的對象返回。當然StringBuffer還是原來的對象。
需要知道的是在Java中所有的基本數據類型都有一個相對應的類,例如Integer類對應int類型,Double類對應double類型等等,這些類也 與String類相同,都是不可以改變的類。也就是說,這些的類中的所有方法都是不能改變其自身的值的。這也讓我們在編clone類的時候有了一個更多的 選擇。同時我們也可以把自己的類編成不可更改的類。
