Java中實(shí)現(xiàn)對(duì)象的拷貝案例講解
對(duì)象的拷貝
簡(jiǎn)介
對(duì)象的拷貝,把一個(gè)對(duì)象中的數(shù)據(jù),復(fù)制到另一個(gè)相同類型的對(duì)象中,在某些情況下,這會(huì)很有用,例如,創(chuàng)建線程安全的實(shí)例中提到的一個(gè)規(guī)范,對(duì)象應(yīng)該返回自身數(shù)據(jù)的拷貝,而不應(yīng)該返回?cái)?shù)據(jù)本身,避免外部對(duì)于數(shù)據(jù)的修改影響到對(duì)象本身,String類就是這么寫(xiě)的,它會(huì)對(duì)外返回char數(shù)組的拷貝,而不是char數(shù)組本身,這樣,當(dāng)用戶修改char數(shù)組時(shí),不會(huì)影響到String對(duì)象本身。
String類的toCharArray方法,返回內(nèi)部char數(shù)組的拷貝
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}淺拷貝和深拷貝
對(duì)象的拷貝,依據(jù)類型的不同,可以分為淺拷貝和深拷貝,淺拷貝是引用復(fù)制,深拷貝是數(shù)據(jù)復(fù)制。
淺拷貝和深拷貝:
- 淺拷貝:將原對(duì)象中的所有字段復(fù)制到新對(duì)象中,如果字段是基本數(shù)據(jù)類型,則復(fù)制的是值,如果字段是引用數(shù)據(jù)類型,則復(fù)制的是指向引用的地址,此時(shí),新對(duì)象和原對(duì)象之間存在共享數(shù)據(jù),如果某個(gè)對(duì)象修改了這部分?jǐn)?shù)據(jù),另一個(gè)對(duì)象也會(huì)受影響
- 深拷貝:不僅將源對(duì)象中所有字段復(fù)制到新對(duì)象中,還會(huì)遞歸地復(fù)制字段引用的對(duì)象,在深拷貝結(jié)束后,原對(duì)象和新對(duì)象之間不存在任何共享數(shù)據(jù),某個(gè)對(duì)象被修改,不會(huì)影響到另一個(gè)對(duì)象。
淺拷貝
Object.clone方法,淺拷貝通常是通過(guò)這個(gè)方法來(lái)實(shí)現(xiàn),它是一個(gè)本地方法,位于Object類中,子類也可以重寫(xiě)它來(lái)實(shí)現(xiàn)深拷貝,但是通常不需要這么做,因?yàn)榇鷥r(jià)比較大,如果希望深拷貝是一項(xiàng)通用能力的話。
方法簽名:protected native Object clone() throws CloneNotSupportedException;
clone方法的使用:
- clone()方法是protected的,這意味著只有子類和同一個(gè)包中的其他類可以調(diào)用它。
- 如果一個(gè)類沒(méi)有實(shí)現(xiàn)Cloneable接口,嘗試調(diào)用它的clone()方法將拋出CloneNotSupportedException。
- 當(dāng)用需要克隆一個(gè)對(duì)象時(shí),需要會(huì)在子類中重寫(xiě)clone()方法,并確保它是public的,這樣外部代碼才能調(diào)用它。
clone方法的底層原理:clone()方法創(chuàng)建了一個(gè)對(duì)象的淺拷貝。這意味著新對(duì)象和原始對(duì)象在內(nèi)存中是兩個(gè)獨(dú)立的副本,但是它們中的引用字段指向相同的對(duì)象。
案例:
// Person類
public class Person implements Cloneable{
private String name;
private int age;
@Override
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
// 省略構(gòu)造方法、getter、setter、toString
}
// PersonGroup類:持有Person類的引用,通過(guò)這個(gè)引用,就可以看出深拷貝和淺拷貝的區(qū)別
public class PersonGroup implements Cloneable {
private String groupName;
private Person person1;
@Override
public PersonGroup clone() throws CloneNotSupportedException {
return (PersonGroup) super.clone();
}
// 省略構(gòu)造方法、getter、setter、toString
}
// 測(cè)試
public class Test3 {
public static void main(String[] args) throws CloneNotSupportedException {
Person person = new Person("張三", 18);
PersonGroup personGroup = new PersonGroup("aaa", person);
PersonGroup clone = personGroup.clone();
System.out.println("clone = " + clone);
}
}對(duì)測(cè)試類進(jìn)行debug,觀察克隆出的實(shí)例和原實(shí)例的內(nèi)存地址,以及它們中成員變量的內(nèi)存地址,可以得出結(jié)論:Object類中的clone方法,將原實(shí)例在堆內(nèi)存中的數(shù)據(jù)復(fù)制了一份,到新的內(nèi)存空間,再將新內(nèi)存空間的地址賦值給clone后的對(duì)象,兩塊內(nèi)存地址中如果有引用類型的數(shù)據(jù),那么它們指向相同的內(nèi)存空間
關(guān)于淺拷貝,有一個(gè)地方值得注意,在原對(duì)象中有引用類型的數(shù)據(jù)的情況下,淺拷貝出的新對(duì)象,如果修改了這個(gè)引用指向的對(duì)象,原對(duì)象也會(huì)受影響,如果是修改了引用本身的指向,那么新對(duì)象不受影響。
深拷貝
深拷貝有多種實(shí)現(xiàn)方式,包括基于IO流的深拷貝、基于json轉(zhuǎn)換的深拷貝,核心原理都是把原對(duì)象和它引用的對(duì)象,全部復(fù)制一份,然后創(chuàng)建一個(gè)新對(duì)象,新對(duì)象的引用也是新對(duì)象。原對(duì)象和新對(duì)象中不存在任何共享對(duì)象。
方式1:使用IO流實(shí)現(xiàn)深拷貝
@SuppressWarnings("unchecked")
public static <T> T deepCopyByIO(T object) {
if (object == null) {
return null;
}
// ByteArrayOutputStream、ByteArrayInputStream,是內(nèi)存流,即使不關(guān)閉也不會(huì)造成資源泄露,例如文件句柄未
// 釋放等問(wèn)題,即使如此,隨時(shí)關(guān)閉流依然是一個(gè)好習(xí)慣,而且關(guān)閉輸出流會(huì)保證緩沖區(qū)的數(shù)據(jù)被正確的寫(xiě)出
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {
objectOutputStream.writeObject(object);
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)) {
return (T) objectInputStream.readObject();
}
} catch (Exception e) {
e.printStackTrace(); // 打印異常信息
return null;
}
}apache提供的common-langs包中也提供了類似的實(shí)現(xiàn),SerializationUtils.clone方法
2、使用json實(shí)現(xiàn)深拷貝
// 這里使用的是gson框架,
public static <T> T deepCopyByGson(T object) {
if (object == null) {
return null;
}
try {
Gson gson = new Gson();
// 獲取對(duì)象的類型
Type type = object.getClass();
// 將對(duì)象轉(zhuǎn)換為 JSON 字符串
String jsonString = gson.toJson(object);
// 從 JSON 字符串解析回對(duì)象
return gson.fromJson(jsonString, type);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}3、使用一些更高效的序列化框架,如hession、kryo等,都可以實(shí)現(xiàn)深拷貝。
深拷貝和循環(huán)引用
循環(huán)引用是指,兩個(gè)對(duì)象互相持有對(duì)方的引用,這種情況下,即使是簡(jiǎn)單的toString方法,如果處理不當(dāng),也可能會(huì)棧內(nèi)存溢出。
案例:循環(huán)引用
// 兩個(gè)對(duì)象互相持有對(duì)方的引用
@Getter
@Setter
public static class Person1 implements Serializable {
private String name;
private Person2 person2;
@Override
public String toString() {
return "Person1{" +
"name='" + name + '\'' +
", person2=" + person2 +
'}';
}
}
@Getter
@Setter
public static class Person2 implements Serializable {
private String name;
private Person1 person1;
@Override
public String toString() {
return "Person2{" +
"name='" + name + '\'' +
", person1=" + person1.toString() +
'}';
}
}
// 測(cè)試
@Test
public void test4() {
Person1 person1 = new Person1();
Person2 person2 = new Person2();
person1.setName("zs");
person1.setPerson2(person2);
person2.setName("ls");
person2.setPerson1(person1);
System.out.println("person1 = " + person1);
}這個(gè)案例會(huì)報(bào)棧內(nèi)存溢出,因?yàn)樵诖蛴?duì)象時(shí),調(diào)用了它的toString方法,打印對(duì)象1的時(shí)候發(fā)現(xiàn)需要打印對(duì)象2,打印對(duì)象2的時(shí)候發(fā)現(xiàn)需要打印對(duì)象1,造成循環(huán)調(diào)用,最終棧內(nèi)存溢出。
所以在深拷貝時(shí),如何解決循環(huán)引用的問(wèn)題?
方式1:使用基于io流的方式,這種方法天然可以避免循環(huán)引用,因?yàn)閷?duì)象序列化機(jī)制內(nèi)置了對(duì)循環(huán)引用的檢測(cè)和處理
方式2:忽略循環(huán)引用相關(guān)的字段,某些json框架可以手動(dòng)配置,忽略某些字段。
案例:使用fastjson來(lái)解決循環(huán)引用的問(wèn)題,這是fastjson內(nèi)置的特性,在最終結(jié)果中,它會(huì)使用 “ref” 來(lái)代替循環(huán)引用
@Test
public void test4() {
Person1 person1 = new Person1();
Person2 person2 = new Person2();
person1.setName("zs");
person1.setPerson2(person2);
person2.setName("ls");
person2.setPerson1(person1);
// 深拷貝
String jsonString = JSON.toJSONString(person1, JSONWriter.Feature.ReferenceDetection);
Person1 person3 = JSON.parseObject(jsonString, Person1.class);
// {"name":"zs","person2":{"name":"ls","person1":{"$ref":"$"}}}
System.out.println("jsonString = " + jsonString);
}總結(jié)
這里介紹了復(fù)制一個(gè)對(duì)象的方法,包括淺拷貝、深拷貝,以及它們的基本使用
到此這篇關(guān)于Java中如何實(shí)現(xiàn)對(duì)象的拷貝的文章就介紹到這了,更多相關(guān)java對(duì)象的拷貝內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot集成redis實(shí)現(xiàn)共享存儲(chǔ)session
這篇文章主要介紹了SpringBoot集成redis實(shí)現(xiàn)共享存儲(chǔ)session的流程步驟,文中通過(guò)代碼示例介紹的非常詳細(xì),并總結(jié)了一些常見(jiàn)的錯(cuò)誤及解決方法,需要的朋友可以參考下2024-03-03
java Long類型轉(zhuǎn)為String類型的兩種方式及區(qū)別說(shuō)明
這篇文章主要介紹了java Long類型轉(zhuǎn)為String類型的兩種方式及區(qū)別說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
探究Android系統(tǒng)中解析JSON數(shù)據(jù)的方式
這篇文章主要介紹了探究Android系統(tǒng)中解析JSON數(shù)據(jù)的方式,文中講到了使用Java代碼實(shí)現(xiàn)的處理JSON的一些主要方法,需要的朋友可以參考下2015-07-07
IDEA2021.2永久激活碼最新超詳細(xì)(激活到2099)
這篇文章主要介紹了IDEA2021.2永久激活碼,是idea2021版最新激活方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09
Java中使用Hutool的DsFactory操作多數(shù)據(jù)源的實(shí)現(xiàn)
在Java開(kāi)發(fā)中,管理多個(gè)數(shù)據(jù)源是一項(xiàng)常見(jiàn)需求,Hutool作為一個(gè)全能的Java工具類庫(kù),提供了DsFactory工具,幫助開(kāi)發(fā)者便捷地操作多數(shù)據(jù)源,感興趣的可以了解一下2024-09-09
java?Comparable和Comparator的區(qū)別及作用面試精講
這篇文章主要為大家介紹了java?Comparable和Comparator的區(qū)別及作用面試精講,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10

