深入理解Java中的克隆
前言
Java克隆(Clone)是Java語言的特性之一,但在實(shí)際中應(yīng)用比較少見。但有時候用克隆會更方便更有效率。
對于克隆(Clone),Java有一些限制:
1、被克隆的類必須自己實(shí)現(xiàn)Cloneable 接口,以指示 Object.clone() 方法可以合法地對該類實(shí)例進(jìn)行按字段復(fù)制。Cloneable 接口實(shí)際上是個標(biāo)識接口,沒有任何接口方法。
2、實(shí)現(xiàn)Cloneable接口的類應(yīng)該使用公共方法重寫 Object.clone(它是受保護(hù)的)。某個對象實(shí)現(xiàn)了此接口就克隆它是不可能的。即使 clone 方法是反射性調(diào)用的,也無法保證它將獲得成功。
3、在Java.lang.Object類中克隆方法是這么定義的:
protected Object clone()
throws CloneNotSupportedException
創(chuàng)建并返回此對象的一個副本。表明是一個受保護(hù)的方法,同一個包中可見。
按照慣例,返回的對象應(yīng)該通過調(diào)用 super.clone 獲得。
Java中的賦值
在Java中,賦值是很常用的,一個簡單的賦值如下
//原始類型 int a = 1; int b = a; //引用類型 String[] weekdays = new String[5]; String[] gongzuori = weekdays;//僅拷貝引用
在上述代碼中。
1、如果是原始數(shù)據(jù)類型,賦值傳遞的為真實(shí)的值
2、如果是引用數(shù)據(jù)類型,賦值傳遞的為對象的引用,而不是對象。
了解了數(shù)據(jù)類型和引用類型的這個區(qū)別,便于我們了解clone。
Clone
在Java中,clone是將已有對象在內(nèi)存中復(fù)制出另一個與之相同的對象的過程。java中的克隆為逐域復(fù)制。
在Java中想要支持clone方法,需要首先實(shí)現(xiàn)Cloneable接口
Cloneable其實(shí)是有點(diǎn)奇怪的,它不同與我們常用到的接口,它內(nèi)部不包含任何方法,它僅僅是一個標(biāo)記接口。
其源碼如下
public interface Cloneable {
}
關(guān)于cloneable,需要注意的
1、如果想要支持clone,就需要實(shí)現(xiàn)Cloneable 接口
2、如果沒有實(shí)現(xiàn)Cloneable接口的調(diào)用clone方法,會拋出CloneNotSupportedException異常。
然后是重寫clone方法,并修改成public訪問級別
static class CloneableImp implements Cloneable {
public int count;
public Child child;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
調(diào)用clone方法復(fù)制對象
CloneableImp imp1 = new CloneableImp();
imp1.child = new Child("Andy");
try {
Object obj = imp1.clone();
CloneableImp imp2 = (CloneableImp)obj;
System.out.println("main imp2.child.name=" + imp2.child.name);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
淺拷貝
上面的代碼實(shí)現(xiàn)的clone實(shí)際上是屬于淺拷貝(Shallow Copy)。
關(guān)于淺拷貝,你該了解的
1、使用默認(rèn)的clone方法
2、對于原始數(shù)據(jù)域進(jìn)行值拷貝
3、對于引用類型僅拷貝引用
4、執(zhí)行快,效率高
5、不能做到數(shù)據(jù)的100%分離。
6、如果一個對象只包含原始數(shù)據(jù)域或者不可變對象域,推薦使用淺拷貝。
關(guān)于無法做到數(shù)據(jù)分離,我們可以使用這段代碼驗(yàn)證
CloneableImp imp1 = new CloneableImp();
imp1.child = new Child("Andy");
try {
Object obj = imp1.clone();
CloneableImp imp2 = (CloneableImp)obj;
imp2.child.name = "Bob";
System.out.println("main imp1.child.name=" + imp1.child.name);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
上述代碼我們使用了imp1的clone方法克隆出imp2,然后修改 imp2.child.name 為 Bob,然后打印imp1.child.name 得到的結(jié)果是
main imp1.child.name=Bob
原因是淺拷貝并沒有做到數(shù)據(jù)的100%分離,imp1和imp2共享同一個Child對象,所以一個修改會影響到另一個。
深拷貝
深拷貝可以解決數(shù)據(jù)100%分離的問題。只需要對上面代碼進(jìn)行一些修改即可。
1、Child實(shí)現(xiàn)Cloneable接口。
public class Child implements Cloneable{
public String name;
public Child(String name) {
this.name = name;
}
@Override
public String toString() {
return "Child [name=" + name + "]";
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
2.重寫clone方法,調(diào)用數(shù)據(jù)域的clone方法。
static class CloneableImp implements Cloneable {
public int count;
public Child child;
@Override
public Object clone() throws CloneNotSupportedException {
CloneableImp obj = (CloneableImp)super.clone();
obj.child = (Child) child.clone();
return obj;
}
}
當(dāng)我們再次修改imp2.child.name就不會影響到imp1.child.name的值了,因?yàn)閕mp1和imp2各自擁有自己的child對象,因?yàn)樽龅搅藬?shù)據(jù)的100%隔離。
關(guān)于深拷貝的一些特點(diǎn)
1、需要重寫clone方法,不僅僅只調(diào)用父類的方法,還需調(diào)用屬性的clone方法
2、做到了原對象與克隆對象之間100%數(shù)據(jù)分離
3、如果是對象存在引用類型的屬性,建議使用深拷貝
4、深拷貝比淺拷貝要更加耗時,效率更低
為什么使用克隆
很重要并且常見的常見就是:某個API需要提供一個List集合,但是又不希望調(diào)用者的修改影響到自身的變化,因此需要克隆一份對象,以此達(dá)到數(shù)據(jù)隔離的目的。
應(yīng)盡量避免clone
1.通常情況下,實(shí)現(xiàn)接口是為了表明類可以為它的客戶做些什么,而Cloneable僅僅是一個標(biāo)記接口,而且還改變了超類中的手保護(hù)的方法的行為,是接口的一種極端非典型的用法,不值得效仿。
2.Clone方法約定及其脆弱 clone方法的Javadoc描述有點(diǎn)曖昧模糊,如下為 Java SE8的約定
clone方法創(chuàng)建并返回該對象的一個拷貝。而拷貝的精確含義取決于該對象的類。一般的含義是,對于任何對象x,表達(dá)式
x.clone() != x 為 true x.clone().getClass() == x.getClass() 也返回true,但非必須 x.clone().equals(x) 也返回true,但也不是必須的
上面的第二個和第三個表達(dá)式很容易就返回false。因而唯一能保證永久為true的就是表達(dá)式一,即兩個對象為獨(dú)立的對象。
3.可變對象final域 在克隆方法中,如果我們需要對可變對象的final域也進(jìn)行拷貝,由于final的限制,所以實(shí)際上是無法編譯通過的。因此為了實(shí)現(xiàn)克隆,我們需要考慮舍去該可變對象域的final關(guān)鍵字。
4.線程安全 如果你決定用線程安全的類實(shí)現(xiàn)Cloneable接口,需要保證它的clone方法做好同步工作。默認(rèn)的Object.clone方法是沒有做同步的。
總的來說,java中的clone方法實(shí)際上并不是完善的,建議盡量避免使用。如下是一些替代方案。
Copy constructors
使用復(fù)制構(gòu)造器也可以實(shí)現(xiàn)對象的拷貝。
1、復(fù)制構(gòu)造器也是構(gòu)造器的一種
2、只接受一個參數(shù),參數(shù)類型為當(dāng)前的類
3、目的是生成一個與參數(shù)相同的新對象
4、復(fù)制構(gòu)造器相比clone方法的優(yōu)勢是簡單,易于實(shí)現(xiàn)。
一段使用了復(fù)制構(gòu)造器的代碼示例
public class Car {
Wheel wheel;
String manufacturer;
public Car(Wheel wheel, String manufacturer) {
this.wheel = wheel;
this.manufacturer = manufacturer;
}
//copy constructor
public Car(Car car) {
this(car.wheel, car.manufacturer);
}
public static class Wheel {
String brand;
}
}
注意,上面的代碼實(shí)現(xiàn)為淺拷貝,如果想要實(shí)現(xiàn)深拷貝,參考如下代碼
//copy constructor
public Car(Car car) {
Wheel wheel = new Wheel();
wheel.brand = car.wheel.brand;
this.wheel = wheel;
this.manufacturer = car.manufacturer;
}
為了更加便捷,我們還可以為上述類增加一個靜態(tài)的方法
public static Car newInstance(Car car) {
return new Car(car);
}
使用Serializable實(shí)現(xiàn)深拷貝
其實(shí),使用序列化也可以實(shí)現(xiàn)對象的深拷貝。簡略代碼如下
public class DeepCopyExample implements Serializable{
private static final long serialVersionUID = 6098694917984051357L;
public Child child;
public DeepCopyExample copy() {
DeepCopyExample copy = null;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
copy = (DeepCopyExample) ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return copy;
}
}
其中,Child必須實(shí)現(xiàn)Serializable接口
public class Child implements Serializable{
private static final long serialVersionUID = 6832122780722711261L;
public String name = "";
public Child(String name) {
this.name = name;
}
@Override
public String toString() {
return "Child [name=" + name + "]";
}
}
使用示例兼測試代碼
DeepCopyExample example = new DeepCopyExample();
example.child = new Child("Example");
DeepCopyExample copy = example.copy();
if (copy != null) {
copy.child.name = "Copied";
System.out.println("example.child=" + example.child + ";copy.child=" + copy.child);
}
//輸出結(jié)果:example.child=Child [name=Example];copy.child=Child [name=Copied]
由輸出結(jié)果來看,copy對象的child值修改不影響example對象的child值,即使用序列化可以實(shí)現(xiàn)對象的深拷貝。
總結(jié)
以上就是Java中克隆的全部內(nèi)容,希望本文對大家學(xué)習(xí)Java能有所幫助。
相關(guān)文章
Javaweb實(shí)現(xiàn)上傳下載文件的多種方法
本篇文章主要介紹了Javaweb實(shí)現(xiàn)上傳下載文件,有多種實(shí)現(xiàn)方式,需要的朋友可以參考下。2016-10-10
Java RPC框架如何實(shí)現(xiàn)客戶端限流配置
這篇文章主要介紹了Java RPC框架如何實(shí)現(xiàn)客戶端限流配置,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-02-02
Java中的動態(tài)代理實(shí)現(xiàn)代碼實(shí)例
這篇文章主要介紹了Java中的動態(tài)代理實(shí)現(xiàn)代碼實(shí)例,jdk動態(tài)代理本質(zhì)上是使用被代理對象的類加載器,通過被代理類實(shí)現(xiàn)的接口在運(yùn)行時動態(tài)構(gòu)造出代理類來增強(qiáng)原始類的功能的方法,需要的朋友可以參考下2023-12-12
解決@NonNull @org.jetbrains.annotations.NotNull報紅的問題
這篇文章主要介紹了解決@NonNull @org.jetbrains.annotations.NotNull報紅的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01
并行Stream與Spring事務(wù)相遇會發(fā)生什么?
這篇文章主要介紹了并行Stream與Spring事務(wù)相遇會發(fā)生什么?文章主要解決實(shí)戰(zhàn)中的Bug及解決方案和技術(shù)延伸,具有一定的參考價值,需要的小伙伴可以參考一下2022-05-05
如何使用BeanUtils.copyProperties進(jìn)行對象之間的屬性賦值
這篇文章主要介紹了使用BeanUtils.copyProperties進(jìn)行對象之間的屬性賦值,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05

