JAVA中clone方法詳解
為了理解java的clone,有必要先了解一些東西。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的引用,鎖的管理等等內務事務所佔記憶體),應該有一個
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的東西,會影響到原型的內部。比如例子中,改變copy的user的name,影響了原型。如果我們要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的,但是這個是在使用B的A中做的,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和不可變型別引用也克隆好了,可變型別引用都是淺copy。3 把淺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 如果class的field是個final的可變類,就不行了。三部曲的第三步沒有辦法做了。考慮一個類對clone的態度,有如下幾種。1 公開支援:好吧,按照clone三部曲實現吧。前提是父類支援(公開或者默默)。2 默默支援:不實現Cloneable介面,但是在類裡面有正確的protected的clone實現,這樣,該類不支援clone,但是它的子類如果想支援的話也不妨礙。3 不支援:好吧,為了明確該目的,提供一個拋CloneNotSupportedException異常的protected的clone實現。4 看情況支援:該類內部可以儲存其他類的例項,如果其他類支援則該類支援,如果其他類不支援,該類沒有辦法,只有不支援。其他的選擇可以用原型建構函式,或者靜態copy方法來手工製作一個物件的copy。好處是即使class的field為final,也不會影響該方法的使用。不好的地方是所有的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.淺複製與深複製概念
⑴淺複製(淺克隆)
被複制物件的所有變數都含有與原來的物件相同的值,而所有的對其他物件的引用仍然指向
原來的物件。換言之,淺複製僅僅複製所考慮的物件,而不復制它所引用的物件。
⑵深複製(深克隆)
被複制物件的所有變數都含有與原來的物件相同的值,除去那些引用其他物件的變數。那些
引用其他物件的變數將指向被複制過的新物件,而不再是原有的那些被引用的物件。換言之,深複製把要複製的物件所引用的物件都複製了一遍。
2.Java的clone()方法
⑴clone方法將物件複製了一份並返回給呼叫者。一般而言,clone()方法滿足:
①對任何的物件x,都有x.clone() !=x//克隆物件與原物件不是同一個物件
②對任何的物件x,都有x.clone().getClass()= =x.getClass()//克隆物件與原物件的型別一樣
③如果物件x的equals()方法定義恰當,那麼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的值。
}
說明:
①為什麼我們在派生類中覆蓋Object的clone()方法時,一定要呼叫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,age為30。
}
那應該如何實現深層次的克隆,即修改s2的教授不會影響s1的教授?程式碼改進如下。
改進使學生1的Professor不改變(深層次的克隆)
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的教授不改變。
}