象拷貝全解析:從淺拷貝到深拷貝的實(shí)現(xiàn)與工具選型)
直接講對(duì)象拷貝這個(gè)問(wèn)題吧。Java里深拷貝和淺拷貝是老話題了但每次聊都有新收獲。我自己踩過(guò)不少坑也看過(guò)很多人寫(xiě)代碼時(shí)在這上面翻車(chē)比如把一個(gè)對(duì)象的屬性隨手復(fù)制給另一個(gè)對(duì)象改了一個(gè)另一個(gè)也跟著變查了半天才發(fā)現(xiàn)是引用拷貝鬧的。這篇就把前因后果、實(shí)現(xiàn)方式、工具選型、避坑經(jīng)驗(yàn)一次說(shuō)透偏實(shí)戰(zhàn)新手能跟上有經(jīng)驗(yàn)的朋友也能看看有沒(méi)有漏掉的東西。1. 對(duì)象拷貝到底是什么問(wèn)題1.1 從“賦值”說(shuō)起最容易被誤解的就是賦值操作??催@段代碼User user1 new User(1, 小王); User user2 user1; // 這不是拷貝 user2.setName(小李); System.out.println(user1.getName()); // 小李這里user1和user2指向的是堆內(nèi)存里的同一個(gè)對(duì)象。不管用哪個(gè)變量改數(shù)據(jù)另一個(gè)“看到”的結(jié)果一樣。很多剛接觸Java的人在這就容易懵明明寫(xiě)了兩行代碼怎么改一個(gè)另一個(gè)也變?cè)蛟贘ava內(nèi)存模型里但用生活類比更好理解——這不叫拷貝只是拿了兩張寫(xiě)著同一個(gè)門(mén)牌號(hào)的紙條。你從哪個(gè)門(mén)進(jìn)去房間里的東西都一樣。提示要判斷一個(gè)操作到底是不是拷貝核心就看一件事——操作之后內(nèi)存里到底多了一個(gè)對(duì)象還是僅僅多了一個(gè)引用。1.2 淺拷貝和深拷貝的本質(zhì)區(qū)別淺拷貝淺復(fù)制會(huì)創(chuàng)建一個(gè)新對(duì)象這個(gè)新對(duì)象的基本類型字段會(huì)被復(fù)制一份獨(dú)立的值但對(duì)象類型字段仍然指向原來(lái)那個(gè)引用。說(shuō)人話就是新對(duì)象是建出來(lái)了但里面裝的對(duì)象還是同一批。深拷貝深復(fù)制要求的是完全獨(dú)立的副本基本類型復(fù)制值引用類型也要遞歸地復(fù)制出一個(gè)新對(duì)象來(lái)。新對(duì)象和舊對(duì)象之間沒(méi)有任何共享的內(nèi)部對(duì)象。畫(huà)個(gè)簡(jiǎn)單的對(duì)照維度淺拷貝深拷貝基本類型字段值復(fù)制獨(dú)立值復(fù)制獨(dú)立引用類型字段共享同一個(gè)引用遞歸復(fù)制完全獨(dú)立修改內(nèi)嵌對(duì)象會(huì)影響原對(duì)象互不影響實(shí)現(xiàn)成本低高需要處理嵌套層級(jí)典型誤區(qū)默認(rèn)clone行為漏掉深層的引用字段1.3 為什么不能一刀切地說(shuō)“深拷貝更好”很多經(jīng)驗(yàn)不深的朋友一聽(tīng)到深拷貝就興奮覺(jué)得既然它更“徹底”那無(wú)腦深拷貝不就完了代碼里確實(shí)也有不少人是這么干的。但用過(guò)幾次就會(huì)發(fā)現(xiàn)深拷貝是有代價(jià)的性能開(kāi)銷嵌套層次越深要?jiǎng)?chuàng)建的對(duì)象越多。復(fù)雜度過(guò)高對(duì)象里如果存在循環(huán)引用搞不好就死循環(huán)或爆棧。場(chǎng)景錯(cuò)配如果在只讀場(chǎng)景里拷貝深拷貝收益為零白捐了一截性能。實(shí)際開(kāi)發(fā)中正確的做法是看場(chǎng)景。緩存讀取、DTO轉(zhuǎn)換、配置快照、中間層防篡改各有各的合適拷貝深度。這一點(diǎn)在后面章節(jié)結(jié)合實(shí)際場(chǎng)景細(xì)說(shuō)。2. 先把引用、對(duì)象、內(nèi)存這三件事搞明白2.1 內(nèi)存模型怎么看“深”和“淺”Java棧上放的是基本類型變量和對(duì)象引用堆上放的是真正的對(duì)象實(shí)例。一個(gè)對(duì)象的字段如果是另一個(gè)對(duì)象棧里的引用就好比一根繩子綁在堆里某個(gè)對(duì)象上。淺拷貝實(shí)現(xiàn)時(shí)新對(duì)象的字段只是把這根繩子又復(fù)制了一根兩根繩子拴的是同一個(gè)氣球。深拷貝則要把那個(gè)氣球也復(fù)制一個(gè)新繩子拴新氣球。這就能很自然地解釋“改一個(gè)另一個(gè)也跟著變”的現(xiàn)象——繩子不同氣球卻還是同一個(gè)。2.2 可變對(duì)象與不可變對(duì)象的差異不可變對(duì)象如String、包裝類、LocalDateTime本身不會(huì)被修改所以即使淺拷貝讓它們共享引用也不存在“一方修改影響另一方”的問(wèn)題。真正要警惕的是可變對(duì)象自定義業(yè)務(wù)對(duì)象、集合、MQ消息體、配置對(duì)象這類。所以有個(gè)實(shí)用經(jīng)驗(yàn)做淺拷貝時(shí)先識(shí)別所有字段里的可變對(duì)象這些才是風(fēng)險(xiǎn)點(diǎn)。2.3 循環(huán)引用是個(gè)隱藏炸彈對(duì)象A里有BB里有A這在實(shí)際業(yè)務(wù)對(duì)象里很常見(jiàn)比如數(shù)據(jù)庫(kù)實(shí)體雙向關(guān)聯(lián)、菜單樹(shù)父子互指。淺拷貝還無(wú)所謂反正共享引用。但深拷貝如果處理不當(dāng)會(huì)陷入無(wú)限遞歸。這里分享一個(gè)判斷方法當(dāng)你準(zhǔn)備對(duì)一個(gè)對(duì)象做深拷貝先頭腦里跑一遍依賴圖但凡發(fā)現(xiàn)有環(huán)就不能用樸素遞歸方案得考慮打破循環(huán)引用或換別的實(shí)現(xiàn)路線。3. 淺拷貝四種常用實(shí)現(xiàn)與陷阱3.1 重寫(xiě)clone()方法的標(biāo)準(zhǔn)手勢(shì)用Object自帶的clone()算是最原生的一種方式。前提是實(shí)現(xiàn)了Cloneable接口不然會(huì)拋CloneNotSupportedException。實(shí)際操作中有幾個(gè)細(xì)節(jié)很多人會(huì)寫(xiě)錯(cuò)Override public Object clone() throws CloneNotSupportedException { return super.clone(); }很多人誤以為這段代碼就完事了實(shí)際上這只是淺拷貝。如果類里有一個(gè)Address address字段user.clone()得到的user2和user1共享同一個(gè)address對(duì)象。這對(duì)某些業(yè)務(wù)來(lái)說(shuō)沒(méi)問(wèn)題但如果你本意是想把用戶連同地址信息一起復(fù)制出來(lái)那這里就埋雷了。注意基本類型和String可以直接修改不會(huì)互相影響開(kāi)發(fā)中更容易踩坑的是集合類字段、自定義對(duì)象字段。3.2 構(gòu)造器拷貝與手動(dòng)Getter/Setter最可控也最原始的方式是手動(dòng)賦值寫(xiě)起來(lái)啰嗦但可靠public User(User source) { this.id source.id; this.name source.name; this.address source.address; // 仍然是淺拷貝 }這種方式的優(yōu)勢(shì)是每個(gè)字段是否復(fù)制、是否重新new全由你親手控制完全透明。代價(jià)是字段多的時(shí)候很折磨人而且一旦類加了字段很容易漏改。3.3 工具類的一鍵淺拷貝到底做了什么光是要做一次淺拷貝用BeanUtils.copyPropertiesSpring版或Apache版比手寫(xiě)簡(jiǎn)單得多用法也幾乎一致UserVO vo new UserVO(); BeanUtils.copyProperties(user, vo);但要用好必須先搞清楚這工具內(nèi)部干了三件事創(chuàng)建目標(biāo)對(duì)象如果是new出來(lái)的、遍歷源對(duì)象的所有讀寫(xiě)屬性、把屬性值通過(guò)setter賦給目標(biāo)對(duì)象。賦值過(guò)程中對(duì)象類型屬性并沒(méi)有遞歸復(fù)制所以它就是淺拷貝。工具類的陷阱也得注意屬性類型不同但名稱相同會(huì)怎么處理目標(biāo)類獨(dú)有的字段會(huì)不會(huì)被覆蓋找不到setter時(shí)是報(bào)錯(cuò)還是靜默跳過(guò)不同版本行為不完全一樣但大體邏輯是盡量兼容匹配不上就忽略。3.4 Spring的BeanUtils和Apache的BeanUtils差異這兩個(gè)名字一模一樣但實(shí)現(xiàn)細(xì)節(jié)差別不小。用下來(lái)最明顯的有兩點(diǎn)Spring的會(huì)在屬性類型不匹配時(shí)直接拋異常Apache的會(huì)盡可能做類型轉(zhuǎn)換實(shí)在不能轉(zhuǎn)才報(bào)錯(cuò)。性能方面Spring的版本還做了一些緩存優(yōu)化多次使用后明顯更快。所以我個(gè)人傾向Spring的異常明確調(diào)試方便。當(dāng)然這只是習(xí)慣如果項(xiàng)目本來(lái)就沒(méi)用Spring直接引入Apache也沒(méi)毛病。4. 深拷貝的實(shí)現(xiàn)方案從手寫(xiě)遞歸到JSON序列化4.1 手動(dòng)深拷貝逐層處理強(qiáng)可控最穩(wěn)的方案還是手動(dòng)寫(xiě)拷貝構(gòu)造函數(shù)或工廠方法里面把可變引用字段也重建一份public User deepCopy() { User copy new User(); copy.setId(this.id); copy.setName(this.name); Address newAddr new Address(); newAddr.setCity(this.address.getCity()); copy.setAddress(newAddr); return copy; }如果是集合字段就得clear或new之后再addAll不能直接賦引用。集合里的每個(gè)元素還要看情況決定是否繼續(xù)深拷。這種方式的優(yōu)點(diǎn)是規(guī)則完全由人掌控調(diào)試方便也沒(méi)有序列化方案那些奇奇怪怪的坑。缺點(diǎn)是對(duì)象層次復(fù)雜后代碼量很大。4.2 重寫(xiě)clone()做深拷貝常在河邊走哪有不濕鞋有人會(huì)在clone方法里把引用字段也克隆一遍比如Override public Object clone() throws CloneNotSupportedException { User copy (User) super.clone(); if (this.address ! null) { copy.address (Address) this.address.clone(); } return copy; }前提是Address也得實(shí)現(xiàn)Cloneable并重寫(xiě)clone。整條鏈路缺一環(huán)深拷貝就變成半深半淺。而且這個(gè)方法容易被繼承結(jié)構(gòu)坑到——子類clon時(shí)如果父類沒(méi)有正確實(shí)現(xiàn)行為就很難預(yù)測(cè)了。4.3 序列化深拷貝一份萬(wàn)金油方案Java原生的序列化實(shí)現(xiàn)深拷貝核心思路是把對(duì)象序列化成一串字節(jié)再?gòu)淖止?jié)反序列化成新對(duì)象。只要這個(gè)類實(shí)現(xiàn)了Serializable就能做byte[] bytes serialize(obj); SomeType copy deserialize(bytes);要特別注意類中所有字段都必須是可序列化的包括自定義對(duì)象的類。如果某字段標(biāo)記了transient那一深拷貝這個(gè)字段在副本里就是null這很容易被忽略。為了性能不一定要用ObjectOutputStream從頭寫(xiě)用現(xiàn)成的工具封裝就好Apache Commons Lang里就提供了SerializationUtils.clone()上傳處理好以后一行代碼就能拿到深拷貝對(duì)象。4.4 JSON深拷貝現(xiàn)代Java開(kāi)發(fā)最常用的黑話JSON方式可能是現(xiàn)在最容易上手的深拷貝姿勢(shì)把對(duì)象序列化成JSON字符串再反序列化回目標(biāo)對(duì)象。ObjectMapper mapper new ObjectMapper(); String json mapper.writeValueAsString(user); User copy mapper.readValue(json, User.class);既然有序列化方案為什么還要用JSON主要是兩個(gè)原因兼容性好不要求所有字段類實(shí)現(xiàn)Serializable而且如果目標(biāo)類型是另一個(gè)結(jié)構(gòu)相近的類JSON還能順手做數(shù)據(jù)映射。缺點(diǎn)也比較明顯性能比純Java序列化略差而且對(duì)象里屬性是接口類型或抽象類型時(shí)反序列化需要額外配置類型信息。4.5 循環(huán)引用的處理方案對(duì)比方案循環(huán)引用表現(xiàn)處理難度手動(dòng)深拷貝自己控制遞歸深度低clone鏈?zhǔn)饺菀妆瑮V蠮ava序列化會(huì)拋NotSerializableException或棧溢出中JSONJackson默認(rèn)會(huì)無(wú)限遞歸需配置較高實(shí)際項(xiàng)目里處理有循環(huán)引用的對(duì)象時(shí)我更傾向于先打破循環(huán)引用比如把雙向關(guān)聯(lián)改成單向或者在拷貝前快照出無(wú)環(huán)的子圖然后再做深拷貝。5. 工具選型與性能實(shí)測(cè)心得5.1 各方案一句話點(diǎn)評(píng)Spring BeanUtils.copyProperties淺拷貝首選項(xiàng)目里已經(jīng)在用Spring的話沒(méi)必要額外引庫(kù)。Apache BeanUtils老牌但類型轉(zhuǎn)換行為偏隱晦performance也一般新項(xiàng)目不太推薦。clone()適合簡(jiǎn)單的單一對(duì)象繼承層級(jí)多的時(shí)候慎用。手寫(xiě)深拷貝性能最高、最可控但代碼工作量大。Java序列化簡(jiǎn)單粗暴但有Serializable依賴。JSONJackson/Gson方便還兼帶有數(shù)據(jù)轉(zhuǎn)換功能。MapStruct編譯期生成映射代碼性能高適合DTO/VO類高頻轉(zhuǎn)換。5.2 MapStruct為什么值得高看一眼如果是做接口層DTO轉(zhuǎn)換我特別建議試試MapStruct。它在編譯期就生成好mapper實(shí)現(xiàn)類運(yùn)行時(shí)就是普通Java代碼沒(méi)有反射沒(méi)有序列化開(kāi)銷。性能上甩BeanUtils一條街而且還支持深拷貝配置。Mapper public interface UserMapper { UserMapper INSTANCE Mappers.getMapper(UserMapper.class); UserVO toVO(User user); }復(fù)雜字段轉(zhuǎn)換失敗時(shí)編譯期就會(huì)報(bào)錯(cuò)而不是運(yùn)行到一半才出問(wèn)題。這一點(diǎn)在生產(chǎn)環(huán)境中省心太多了。5.3 性能參考不是選貴的是選對(duì)的我自己在本地做過(guò)一個(gè)粗略測(cè)試對(duì)象是三層嵌套結(jié)構(gòu)創(chuàng)建10萬(wàn)份拷貝數(shù)據(jù)大概是這樣不同電腦結(jié)果有差異看個(gè)量級(jí)就好方式相對(duì)耗時(shí)手動(dòng)復(fù)制get/set1倍clone重寫(xiě)1.2-1.5倍反射BeanUtils8-15倍Java序列化20-30倍Jackson JSON深拷貝30-50倍結(jié)論很清晰如果是每秒鐘上萬(wàn)次的核心鏈路能手動(dòng)就手動(dòng)能MapStruct就MapStruct如果是低頻操作比如后臺(tái)偶爾導(dǎo)出、請(qǐng)求量不高的定時(shí)任務(wù)JSON一把梭完全沒(méi)問(wèn)題。6. 實(shí)戰(zhàn)代碼示例從淺拷貝到深拷貝的完整實(shí)現(xiàn)6.1 基礎(chǔ)模型類準(zhǔn)備好最基礎(chǔ)的兩個(gè)類。User類里面有基本信息、一個(gè)Address對(duì)象和一個(gè)興趣列表public class Address { private String city; private String street; // 構(gòu)造器、getter/setter } public class User { private int id; private String name; private Address address; private ListString hobbies; // 構(gòu)造器、getter/setter }代碼就不全貼了核心是理解里面不同類型字段的變化。6.2 淺拷貝的完整寫(xiě)法用常見(jiàn)的方式做淺拷貝User user new User(1, 小王, new Address(上海, 浦東大道), List.of(跑步, 讀書(shū))); User copy new User(user.getId(), user.getName(), user.getAddress(), user.getHobbies()); // 或 BeanUtils.copyProperties(user, copy);這兩者的效果幾乎一樣copy和user是兩個(gè)不同對(duì)象但copy.getAddress()拿到的仍然和user.getAddress()是同一個(gè)。實(shí)測(cè)驗(yàn)證System.out.println(user copy); // false對(duì)象本身不同 System.out.println(user.getAddress() copy.getAddress()); // true引用相同 copy.getHobbies().add(攝影); System.out.println(user.getHobbies()); // [跑步, 讀書(shū), 攝影]原對(duì)象被影響了6.3 深拷貝的完整寫(xiě)法手寫(xiě)深拷貝方案適合核心鏈路效果見(jiàn)下方代碼public User deepCopy() { User newUser new User(); newUser.setId(this.id); newUser.setName(this.name); if (this.address ! null) { Address newAddr new Address(this.address.getCity(), this.address.getStreet()); newUser.setAddress(newAddr); } if (this.hobbies ! null) { newUser.setHobbies(new ArrayList(this.hobbies)); } return newUser; }后面再驗(yàn)證一下address和list都已是獨(dú)立對(duì)象User deep user.deepCopy(); System.out.println(user.getAddress() deep.getAddress()); // false deep.getHobbies().add(攝影); System.out.println(user.getHobbies()); // [跑步, 讀書(shū)]6.4 用JSON實(shí)現(xiàn)干凈的深拷貝手寫(xiě)深拷貝只是示例。在實(shí)際項(xiàng)目里如果對(duì)象結(jié)構(gòu)不復(fù)雜、又不想手寫(xiě)一大坨直接用JSON最干凈。用JacksonObjectMapper objectMapper new ObjectMapper(); User deepCopy objectMapper.readValue(objectMapper.writeValueAsString(user), User.class);對(duì)象里如果有泛型、多態(tài)、日期等字段需要額外配置比如JavaTimeModule處理LocalDateTime多態(tài)字段用JsonTypeInfo標(biāo)明實(shí)際類型。JSON深拷貝的“深”其實(shí)取決于Jackson是否完整還原了所有嵌套對(duì)象的結(jié)構(gòu)。6.5 多一個(gè)字段少一個(gè)字段時(shí)的兼容處理DTO轉(zhuǎn)換中經(jīng)常出現(xiàn)源對(duì)象和目標(biāo)對(duì)象字段數(shù)量不一致的情況。用BeanUtils是按屬性名匹配的源對(duì)象有而目標(biāo)沒(méi)有的忽略目標(biāo)有但源沒(méi)有的目標(biāo)保持原值。MapStruct則更嚴(yán)格編譯期就會(huì)指出哪些字段沒(méi)映射。如果是JSON反序列化到另一個(gè)類結(jié)果字段缺失一般會(huì)得到默認(rèn)值比如null或0。這個(gè)問(wèn)題看起來(lái)不大但很多線上bug就是這些“靜默默認(rèn)值”惹的禍盡量通過(guò)FAIL_ON_UNKNOWN_PROPERTIES等配置把問(wèn)題盡早暴露出來(lái)。7. 我在實(shí)際開(kāi)發(fā)中踩過(guò)的坑與排查思路7.1 集合拷貝但元素沒(méi)拷改一個(gè)全變有次我在一個(gè)緩存Service里看到這樣的代碼ListOrderDetail detailList orderService.getDetails(orderId); ListOrderDetail copyList new ArrayList(detailList);兩個(gè)list是不同對(duì)象但里面的OrderDetail元素還是同一批對(duì)象。后面有人對(duì)copyList里的某個(gè)OrderDetail改了個(gè)狀態(tài)結(jié)果緩存里的原數(shù)據(jù)也被改了。需要明確new ArrayList(oldList)這叫淺拷貝集合結(jié)構(gòu)不叫深拷貝。如果只是追加、刪除元素這么寫(xiě)沒(méi)問(wèn)題如果要改元素內(nèi)容必須再做一層深拷貝。提示判斷集合拷貝是否是深拷貝的方法拿到新集合后修改其中一個(gè)元素的屬性再看原集合里的元素是否改變。7.2 構(gòu)造一個(gè)“半深不淺”的對(duì)象比淺拷貝還難排查別的對(duì)象定義成可變引用字段后深拷貝總是容易漏掉。比如把id和name復(fù)制的好address也new了但address里還有一層province對(duì)象忘了復(fù)制導(dǎo)致改province時(shí)原對(duì)象跟著變。這種問(wèn)題只能靠設(shè)計(jì)規(guī)避一是盡可能用不可變類型二是深拷貝方法寫(xiě)完后主動(dòng)寫(xiě)單元測(cè)試驗(yàn)證每一層引用是否都已經(jīng)是新對(duì)象。Test void testDeepCopy() { User source buildUser(); User target source.deepCopy(); assertNotSame(source.getAddress(), target.getAddress()); assertNotSame(source.getAddress().getProvince(), target.getAddress().getProvince()); // 逐層斷言 }7.3 使用BeanUtils遇到類型轉(zhuǎn)換異常Spring的BeanUtils在類型不匹配時(shí)直接拋異常有時(shí)只因?yàn)閮蓚€(gè)類都把某字段定義成了不同但兼容的類型比如源是long目標(biāo)是Long都會(huì)被認(rèn)為不匹配。這種情況出現(xiàn)后我會(huì)單獨(dú)手動(dòng)賦值那個(gè)字段或者調(diào)整目標(biāo)類的字段類型不硬剛。7.4 不要忽略null值很多深拷貝實(shí)現(xiàn)沒(méi)有處理null字段結(jié)果原對(duì)象為null拷貝后卻自動(dòng)有了一個(gè)空對(duì)象甚至空集合。這在業(yè)務(wù)上可能造成語(yǔ)義變化——null和空集合不是一回事。實(shí)現(xiàn)時(shí)要明確是否保留nullNull值處理策略要統(tǒng)一。8. 如何優(yōu)雅地設(shè)計(jì)可復(fù)用的拷貝工具8.1 定一個(gè)統(tǒng)一的深拷貝接口要避免每次拷貝都重新寫(xiě)一套可以在工具層統(tǒng)一封裝。比如實(shí)現(xiàn)了Serializable的對(duì)象通過(guò)序列化做深拷貝其他情況用JSON如果是高頻小對(duì)象就手寫(xiě)。public class CopyUtils { public static T T deepCopy(T source, ClassT targetType) throws Exception { ObjectMapper mapper new ObjectMapper() .registerModule(new JavaTimeModule()) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); String json mapper.writeValueAsString(source); return mapper.readValue(json, targetType); } }這樣的工具團(tuán)隊(duì)成員用起來(lái)簡(jiǎn)單也能集中改配置。8.2 區(qū)分業(yè)務(wù)對(duì)象的默認(rèn)拷貝策略我自己的習(xí)慣是實(shí)體對(duì)象Entity和領(lǐng)域?qū)ο驞omain默認(rèn)淺拷貝除非明確要快照或者隔離數(shù)據(jù)DTO/VO轉(zhuǎn)換盡量用MapStruct跨線程傳輸或需要存放到消息隊(duì)列的對(duì)象用深拷貝加不可變?cè)O(shè)計(jì)。這么做的理由是實(shí)體對(duì)象通常位于事務(wù)邊界內(nèi)深拷貝會(huì)隔斷上下文狀態(tài)同步還可能導(dǎo)致一些懶加載字段被序列化時(shí)出問(wèn)題。8.3 注解或配置驅(qū)動(dòng)的拷貝策略在對(duì)象拷貝需求越來(lái)越復(fù)雜時(shí)可以考慮在字段上加自定義注解比如CopyIgnore、NestedCopy然后通過(guò)反射解析注解來(lái)決定某個(gè)字段是忽略引用還是遞歸深拷貝。這個(gè)方案靈活但實(shí)現(xiàn)復(fù)雜度不低適合團(tuán)隊(duì)已經(jīng)有底層公共能力的項(xiàng)目小項(xiàng)目不建議上這種重量級(jí)封裝。9. 對(duì)象拷貝不是一個(gè)“無(wú)腦復(fù)制”的功能說(shuō)句實(shí)話對(duì)象拷貝在Java開(kāi)發(fā)里看著很簡(jiǎn)單但它牽扯著內(nèi)存布局、類設(shè)計(jì)、序列化機(jī)制、工具庫(kù)行為差異是一個(gè)值得認(rèn)真對(duì)待的設(shè)計(jì)點(diǎn)。選淺拷貝還是深拷貝核心要看對(duì)象生命周期和共享可變狀態(tài)的風(fēng)險(xiǎn)而不是抱著“深拷貝就是更高級(jí)”的想法。9.1 作為性能優(yōu)化點(diǎn)的拷貝策略在熱路徑上能避免拷貝就盡量避免。比如讀操作不需要副本時(shí)直接用原對(duì)象只有跨模塊、跨線程、寫(xiě)緩存時(shí)才需要副本。那些試圖在每個(gè)接口里都復(fù)制一份防篡改的做法往往性能瓶頸就從這里冒出來(lái)。9.2 不可變類的終極方案如果讓對(duì)象本身不可變那連拷貝都省了大家共享引用就絕對(duì)安全?,F(xiàn)代Java開(kāi)發(fā)中盡量把值對(duì)象設(shè)計(jì)成不可變配合Builder模式會(huì)讓代碼極其省心。這是比深拷貝更徹底的一層解法。9.3 從拷貝擴(kuò)展到數(shù)據(jù)映射的思想對(duì)象拷貝的概念稍作延伸就是數(shù)據(jù)映射Bean到DTO、DO到VO、外部API返回的對(duì)象到內(nèi)部模型。理解淺拷貝、深拷貝的那套邏輯后再看MapStruct、ModelMapper這類映射工具會(huì)非常輕松因?yàn)楸举|(zhì)上它們解決的是同一個(gè)問(wèn)題——怎么在對(duì)象之間安全、高效地搬運(yùn)數(shù)據(jù)。我寫(xiě)這篇東西其實(shí)最想傳達(dá)的不是“某個(gè)拷貝工具怎么用”而是每次操作對(duì)象拷貝前多停兩秒想想這個(gè)引用被共享之后它有沒(méi)有可能在別的地方被改掉誰(shuí)改的改了對(duì)線上有什么影響。想清楚這三點(diǎn)你自然知道該用淺拷貝、深拷貝還是干脆不拷貝。