1.為什么需要克驴独蟆?
比如有時(shí)我們要獲得一個(gè)非常復(fù)雜的對(duì)象缠俺,與其使用new創(chuàng)建對(duì)象再對(duì)各個(gè)屬性賦值显晶,不如直接克隆現(xiàn)有的對(duì)象。在java中對(duì)于基本類型可以使用"="進(jìn)行克隆壹士,而對(duì)于引用類型卻不能這樣做磷雇。
為什么不能對(duì)引用類型直接使用“=”呢?
答:因?yàn)閖ava將內(nèi)存空間分為2塊墓卦,即堆和棧倦春。在棧中保存基本類型和引用類型的變量,在堆中保存對(duì)象落剪。如果使用“=”,將修改引用睁本。此時(shí)兩個(gè)引用變量指向同一個(gè)對(duì)象,如果其中的一個(gè)引用變量進(jìn)行修改也會(huì)改變另一個(gè)變量忠怖。
舉例說明:
光頭強(qiáng)-->銀行卡<--灰太狼
光頭強(qiáng)和灰太狼共同擁有一張銀行卡呢堰,無論誰向里面存錢,都會(huì)對(duì)對(duì)方產(chǎn)生影響凡泣。
2.克隆的錯(cuò)誤方式(假克峦魈邸)
package test;
public class Test {
public static void main(String[] args) {
Person p = new Person("懶洋洋",10);
System.out.println("克隆前的Person");
System.out.println(p.toString());
Person pClone = p;
//對(duì)name和age重新賦值
pClone.name = "光頭強(qiáng)";
pClone.age = 80;
System.out.println("克隆后的Person");
System.out.println(pClone.toString());
System.out.println("pClone修改屬性后,輸出p的信息");
System.out.println(p.toString());
}
}
測(cè)試結(jié)果:
克隆前的Person
姓名:懶洋洋 年齡:10
克隆后的Person
姓名:光頭強(qiáng) 年齡:80
pClone修改屬性后鞋拟,輸出p的信息
姓名:光頭強(qiáng) 年齡:80
其他知識(shí)點(diǎn):棧中的知識(shí)點(diǎn)使用完后會(huì)立即被回收骂维,而堆中的對(duì)象是由java虛擬機(jī)管理的,即使該對(duì)象已經(jīng)不再使用贺纲,該內(nèi)存空間只會(huì)在一個(gè)不確定的時(shí)間被回收
3.Java對(duì)象的淺克隆
-
為什么叫淺克潞焦搿?
答:因?yàn)閷?duì)基本類型的域(各個(gè)屬性或方法)的復(fù)制是行之有效的,而對(duì)于引用類型的域可能會(huì)出現(xiàn)錯(cuò)誤潦刃。但是它避免了“一個(gè)引用變量進(jìn)行修改影響另一個(gè)”的問題侮措。
-
需要實(shí)現(xiàn)的接口
答:需要實(shí)現(xiàn)Cloneable接口,如果不實(shí)現(xiàn)這個(gè)接口乖杠,clone()方法會(huì)拋出CloneNotSupportedException異常分扎。
代碼測(cè)試:
package test;
/**
* 創(chuàng)建一個(gè)狗類
*/
public class Dog {
public String dogName;
public String dogBreed;//狗的品種
public Dog(String dogName, String dogType) {
this.dogName = dogName;
this.dogBreed = dogType;
}
public String toString(){
StringBuffer sb = new StringBuffer();
sb.append("狗的名字:"+dogName+",")
.append("狗的品種:"+dogBreed);
return sb.toString();
}
}
再創(chuàng)建一個(gè)人類:
package test;
/**
* 創(chuàng)建一個(gè)人類
* @author hgx
*
*/
public class Person implements Cloneable{
public String name;
public int age;
Dog dog = null;
public Person(){
}
public Person(String name, int age, Dog dog) {
this.name = name;
this.age = age;
this.dog = dog;
}
//實(shí)現(xiàn)Cloneable接口,重寫clone()方法
public Person clone(){
Person p = null;
try {
p = (Person) super.clone();
} catch (Exception e) {
e.printStackTrace();
}
return p;
}
//每個(gè)類都有toString()方法胧洒,我們可以對(duì)其進(jìn)行重寫
public String toString(){
StringBuffer sb = new StringBuffer();
sb.append("人的名字:"+name+",")
.append("人的年齡:"+age+",")
.append("所擁有的狗:"+dog);
return sb.toString();
}
}
測(cè)試類:
package test;
public class Test {
public static void main(String[] args) {
Dog dog = new Dog("希密士", "哈士奇");
Person p = new Person("懶洋洋", 10, dog);
System.out.println("克隆前的Person");
System.out.println(p.toString());
Person pClone = p.clone();
// 對(duì)name和age重新賦值
pClone.name = "光頭強(qiáng)";
//對(duì)dog進(jìn)行修改
pClone.dog.dogName = "藏獒";
System.out.println("克隆后的Person");
System.out.println(pClone.toString());
System.out.println("pClone修改屬性后畏吓,輸出p的信息");
System.out.println(p.toString());
}
}
測(cè)試結(jié)果:
克隆前的Person
人的名字:懶洋洋,人的年齡:10,所擁有的狗:狗的名字:希密士,狗的品種:哈士奇
克隆后的Person
人的名字:光頭強(qiáng),人的年齡:10,所擁有的狗:狗的名字:藏獒,狗的品種:哈士奇
pClone修改屬性后,輸出p的信息
人的名字:懶洋洋,人的年齡:10,所擁有的狗:狗的名字:藏獒,狗的品種:哈士奇
總結(jié):我們通過結(jié)果可以發(fā)現(xiàn)略荡,克隆后的pClone雖然修改了name和age屬性庵佣,但對(duì)原來p的屬性并沒有影響。但當(dāng)我們對(duì)dog這個(gè)引用進(jìn)行修改后汛兜,原來p的dog也發(fā)生了變化巴粪。這也就說明“淺克隆對(duì)基本類型的復(fù)制小菜一碟,但對(duì)于引用類型的不能完全控制粥谬,除非我們不對(duì)引用類型進(jìn)行修改”
這就是淺克隆存在的問題肛根。但上有政策,下有對(duì)策漏策。遇到深克隆派哲,這些問題都要死掉。
涉及到的其他知識(shí)點(diǎn):StringBuffer掺喻,toString()方法
4.java的深克隆
-
為什叫深克掳沤臁?
這個(gè)問題感耙,在上面我們已經(jīng)了如指掌了褂乍。話不多說,言多必失即硼,看代碼逃片。
由于大部分代碼相同,所以下面只寫改變的地方
代碼案例:
1.Dog類的改變:實(shí)現(xiàn)Cloneable接口只酥,并重寫了clone()方法
//注意不要忘記實(shí)現(xiàn)Cloneable
@Override
protected Dog clone(){
Dog dog = null;
try {
dog = (Dog) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return dog;
}
2.Person類的改變:在clone()方法里添加了Dog類的克隆
@Override
public Person clone(){
Person p = null;
try {
p = (Person) super.clone();
p.dog = dog.clone();
} catch (Exception e) {
e.printStackTrace();
}
return p;
}
測(cè)試結(jié)果:
克隆前的Person
人的名字:懶洋洋,人的年齡:10,所擁有的狗:狗的名字:希密士,狗的品種:哈士奇
克隆后的Person
人的名字:光頭強(qiáng),人的年齡:10,所擁有的狗:狗的名字:藏獒,狗的品種:哈士奇
pClone修改屬性后褥实,輸出p的信息
人的名字:懶洋洋,人的年齡:10,所擁有的狗:狗的名字:希密士,狗的品種:哈士奇
總結(jié):結(jié)果已經(jīng)證明我們的目的達(dá)到了,隨意改變一個(gè)克隆都不會(huì)影響到原來的類裂允。
注意:通常情況下损离,克隆對(duì)象時(shí)都需要使用深克隆,但需要注意的是绝编,要是引用類型中還有引用草冈,就都需要實(shí)現(xiàn)Cloneable接口,重寫clone()方法瓮增。
5.深克隆與淺克隆的區(qū)別
代碼上的不同:淺克隆只是克隆類實(shí)現(xiàn)了Cloneable接口怎棱,而深克隆不僅克隆類實(shí)現(xiàn)了Cloneable接口,而且克隆類里的引用類型也實(shí)現(xiàn)了Cloneable接口绷跑。
功能上不同拳恋,深克隆解決了淺克隆“不能很好對(duì)引用類型的復(fù)制”的問題。
6.序列化與對(duì)象克隆
1.為什么還要使用序列化進(jìn)行對(duì)象的克隆
如果類有很多類型砸捏,而且引用類型里還有引用類型的話谬运,那我們豈不是每個(gè)都要重寫clone()方法,這樣也太麻煩了垦藏。所以序列化就是來解決這個(gè)難題的梆暖。
2.序列化是什么?
序列化掂骏,簡(jiǎn)單的說就像時(shí)間漏斗一樣轰驳,漏斗上面的沙子不停的落下,直到漏完為止弟灼。而在java中的序列化對(duì)象级解,就是這樣一點(diǎn)一點(diǎn)的將一個(gè)對(duì)象復(fù)制成另一個(gè)對(duì)象。其實(shí)田绑,蠻像3D打印機(jī)的勤哗。
3.大體過程
利用輸出流,將對(duì)象以字節(jié)的形式寫入一個(gè)字節(jié)數(shù)組掩驱,然后再通過輸入流芒划,從這個(gè)字節(jié)數(shù)組中讀出。此時(shí)就不需要考慮引用類型的問題了欧穴。因?yàn)樾蛄谢梢哉f民逼,是從根源入手,從本質(zhì)上實(shí)現(xiàn)克隆苔可。就像水管漏水缴挖,只清除水是沒有用的,只有把水管關(guān)住才行焚辅。
生活感悟:所以問題還是要從根源上解決映屋,才可保證萬無一失。如果java中年遇到一些難題同蜻,我們可以想想在生活中是怎樣解決的棚点。
4.需要實(shí)現(xiàn)的接口
Serializable接口
5.例子代碼
package test;
import java.io.Serializable;
// 創(chuàng)建一個(gè)狗類
public class Dog implements Serializable{
public String dogName;
public String dogBreed;//狗的品種
public Dog(String dogName, String dogType) {
this.dogName = dogName;
this.dogBreed = dogType;
}
public String toString(){
StringBuffer sb = new StringBuffer();
sb.append("狗的名字:"+dogName+",")
.append("狗的品種:"+dogBreed);
return sb.toString();
}
}
//人類
package test;
/**
* 創(chuàng)建一個(gè)人類
*/
public class Person implements Cloneable, Serializable {
public String name;
public int age;
Dog dog = null;
public Person() {
}
public Person(String name, int age, Dog dog) {
this.name = name;
this.age = age;
this.dog = dog;
}
@Override
public Person clone() {
Person p = null;
ByteArrayOutputStream bao = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(this);
oos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
try {
ObjectInputStream ois = new ObjectInputStream(bis);
p = (Person) ois.readObject();
ois.close();
} catch (IOException | ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return p;
}
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("人的名字:" + name + ",").append("人的年齡:" + age + ",").append("所擁有的狗:" + dog);
return sb.toString();
}
}
測(cè)試類不變:
測(cè)試結(jié)果:
克隆前的Person
人的名字:懶洋洋,人的年齡:10,所擁有的狗:狗的名字:希密士,狗的品種:哈士奇
克隆后的Person
人的名字:光頭強(qiáng),人的年齡:10,所擁有的狗:狗的名字:藏獒,狗的品種:哈士奇
pClone修改屬性后,輸出p的信息
人的名字:懶洋洋,人的年齡:10,所擁有的狗:狗的名字:希密士,狗的品種:哈士奇
注意:序列化對(duì)象要求實(shí)現(xiàn)Cloneable和Serializable接口湾蔓,如果類中有引用類型的瘫析,則引用類型也需要實(shí)現(xiàn)Serializable接口。在效率上,序列化要比深克隆慢一點(diǎn)贬循。
7.選擇適當(dāng)?shù)目寺》绞?/h2>
如果類的各個(gè)屬性的基本類型和引用類型不會(huì)變咸包,則可以使用淺克隆。否則使用深克隆杖虾。如果類的結(jié)構(gòu)比較復(fù)雜烂瘫,則使用序列化。
8.transient關(guān)鍵字的應(yīng)用
1.為什么使用transient關(guān)鍵字奇适?
比如有一些屬性坟比,我們不想克隆的新的對(duì)象訪問到它的值(比如密碼),則可以在屬性的前面加上transient關(guān)鍵字嚷往。這樣克隆的時(shí)候葛账,雖然變量克隆到新的對(duì)象中了,但是其值為null(如果為引用類型)或0(如果為基本類型)皮仁。
2.transient注意事項(xiàng)
1. transient只能用來修飾屬性籍琳,不能修飾方法
2. 如果使用序列化一個(gè)類,而這個(gè)類中的引用類型又沒有實(shí)現(xiàn)Serializable接口或者不想對(duì)其序列化魂贬,則需要使用transient修飾該引用類型