Java四種拷貝方式實(shí)戰(zhàn)總結(jié)(一文掃清所有拷貝問題)
前言
作為Java開發(fā)者,日常開發(fā)中經(jīng)常會(huì)遇到數(shù)據(jù)拷貝的需求。最近在面試中也有被問到一次大文件拷貝,抽空專門總結(jié)一下,從基礎(chǔ)概念到實(shí)戰(zhàn)技巧,配合流程圖,讓原理和流程一目了然~
一、淺拷貝VS深拷貝
一開始做spring項(xiàng)目時(shí),就被淺拷貝坑過。當(dāng)時(shí)想復(fù)制一個(gè)對(duì)象,結(jié)果改了新對(duì)象的屬性,老對(duì)象也跟著變了。后來才知道,這就是淺拷貝的“鍋”。
淺拷貝就像拍證件照,照片上的人看著和你一模一樣,但本質(zhì)上還是兩張紙。Java里的淺拷貝只會(huì)復(fù)制對(duì)象的基本屬性,遇到引用類型(比如自定義類),就直接把地址抄過來。所以你改新對(duì)象里的引用屬性,老對(duì)象也會(huì)跟著遭殃。
深拷貝就靠譜多了,它會(huì)把對(duì)象里里外外都復(fù)制一遍,就像克隆人,新對(duì)象和老對(duì)象完全獨(dú)立。雖然麻煩點(diǎn),但勝在安全,適合處理復(fù)雜對(duì)象。

舉個(gè)例子,有個(gè)Person類包含name和Address屬性:
class Address {
String city;
}
class Person {
String name;
Address address;
// 淺拷貝
Person shallowCopy() {
Person copy = new Person();
copy.name = this.name;
copy.address = this.address;
return copy;
}
// 深拷貝
Person deepCopy() {
Person copy = new Person();
copy.name = this.name;
copy.address = new Address();
copy.address.city = this.address.city;
return copy;
}
}
二、大文件拷貝:別讓你的程序“龜速運(yùn)行”
之前做文件上傳功能,用傳統(tǒng)IO方式拷貝大文件,結(jié)果服務(wù)器直接卡住。后來?yè)Q成NIO,速度直接起飛!
傳統(tǒng)IO就像螞蟻搬家,一次搬一點(diǎn),頻繁讀寫磁盤;NIO則像卡車運(yùn)輸,直接把數(shù)據(jù)從一個(gè)地方搬到另一個(gè)地方,速度快得多。

// 傳統(tǒng)IO方式,適合小文件
public static void copyFileByIO(File source, File dest) throws IOException {
try (InputStream is = new FileInputStream(source);
OutputStream os = new FileOutputStream(dest)) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
}
}
// NIO方式,適合大文件
public static void copyFileByNIO(File source, File dest) throws IOException {
try (FileChannel sourceChannel = new FileInputStream(source).getChannel();
FileChannel destChannel = new FileOutputStream(dest).getChannel()) {
destChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
}
}
三、對(duì)象拷貝的進(jìn)階方案:使用造好的輪子
手動(dòng)寫深拷貝代碼太麻煩?別慌,Java提供給我們有不少寫好的辦法。
1. 序列化與反序列化
就像把對(duì)象打包成快遞,寄出去再拆開。雖然速度慢點(diǎn),但能保證完全獨(dú)立的拷貝。不過要注意,所有相關(guān)類都得實(shí)現(xiàn)Serializable接口。

public static <T extends Serializable> T deepCopyBySerialization(T obj) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
try (ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais)) {
return (T) ois.readObject();
}
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
2. JSON序列化
用Jackson或Gson把對(duì)象轉(zhuǎn)成JSON字符串,再轉(zhuǎn)回來,簡(jiǎn)單粗暴。不過這種方式要求對(duì)象的屬性都能被JSON正確解析。

import com.fasterxml.jackson.databind.ObjectMapper;
public static <T> T deepCopyByJSON(T obj, Class<T> clazz) {
try {
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(obj);
return mapper.readValue(json, clazz);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
四、數(shù)組和集合的拷貝
數(shù)組的clone()方法看似簡(jiǎn)單,實(shí)則暗藏玄機(jī):基本類型數(shù)組用它是深拷貝,對(duì)象數(shù)組就是淺拷貝。

int[] intArray = {1, 2, 3};
int[] copiedIntArray = intArray.clone(); // 深拷貝
Person[] personArray = new Person[2];
Person[] copiedPersonArray = personArray.clone(); // 淺拷貝
集合類的構(gòu)造函數(shù)也是淺拷貝,比如new ArrayList<>(originalList)。想深拷貝集合,得手動(dòng)遍歷每個(gè)元素。

List<Person> originalList = new ArrayList<>();
List<Person> deepCopiedList = originalList.stream()
.map(Person::deepCopy)
.collect(Collectors.toList());
五、第三方庫(kù):站在巨人的肩膀上
不想自己造輪子?Apache Commons Lang和Dozer能幫你大忙。前者提供了便捷的序列化拷貝工具,后者擅長(zhǎng)對(duì)象屬性映射,用起來超方便。
<!-- Apache Commons Lang依賴 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
// 使用示例
Person copiedPerson = SerializationUtils.clone(originalPerson);
六、總結(jié):看不同場(chǎng)景選擇合適的拷貝方式
- 簡(jiǎn)單對(duì)象用淺拷貝,復(fù)雜對(duì)象用深拷貝
- 大文件優(yōu)先用NIO,小文件用傳統(tǒng)IO
- 能偷懶就偷懶,善用第三方庫(kù)
- 多寫測(cè)試用例,別讓拷貝“埋雷”
到此這篇關(guān)于Java四種拷貝方式實(shí)戰(zhàn)文章就介紹到這了,更多相關(guān)Java四種拷貝方式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于BindingResult的使用總結(jié)及注意事項(xiàng)
這篇文章主要介紹了關(guān)于BindingResult的使用總結(jié)及注意事項(xiàng),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
哲學(xué)家就餐問題中的JAVA多線程學(xué)習(xí)
哲學(xué)家就餐問題是1965年由Dijkstra提出的一種線程同步的問題,下面我們就看一下JAVA多線程如何做2013-11-11
Springboot 2.x集成kafka 2.2.0的示例代碼
kafka近幾年更新非??欤部梢钥闯鰇afka在企業(yè)中是用的頻率越來越高。本文主要為大家介紹了Springboot 2.x集成kafka 2.2.0的示例代碼,需要的可以參考一下2022-04-04
java ExecutorService CompletionService線程池區(qū)別與選擇
這篇文章主要為大家介紹了java ExecutorService CompletionService線程池區(qū)別與選擇使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
springcloud gateway設(shè)置context-path的操作
這篇文章主要介紹了springcloud gateway設(shè)置context-path的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
SpringBoot中給指定接口加上權(quán)限校驗(yàn)的實(shí)現(xiàn)
本文介紹了使用SpringSecurity為接口添加權(quán)限校驗(yàn),以防止外部訪問并確保安全性,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-12-12

