簡單聊聊對象淺拷貝和深拷貝,真不簡單!
一、摘要
上篇文章中,我們有介紹到對象屬性復(fù)制相關(guān)的工具,這些工具所進(jìn)行的對象拷貝,其實都是淺拷貝模式。
可能有的同學(xué)會發(fā)出疑問,什么叫淺拷貝?
我們都知道,Java 中的數(shù)據(jù)類型分為值類型(基本數(shù)據(jù)類型)和引用類型,值類型包括 byte、short、 int、long、float、double、boolean、char 等簡單數(shù)據(jù)類型,引用類型包括類、接口、數(shù)組等復(fù)雜類型。
根據(jù)數(shù)據(jù)類型的不同,在進(jìn)行屬性值拷貝的時候,如果是值類型,復(fù)制的是屬性值,如果是復(fù)雜類型,比如對象,復(fù)制的內(nèi)容可能是屬性對應(yīng)的內(nèi)存引用地址。
因此,在 Java 中對于復(fù)雜類型的數(shù)據(jù),也分為**淺拷貝(淺克隆)與深拷貝(深克隆)**方式,區(qū)別如下:
- 淺拷貝:將原對象或原數(shù)組的引用直接賦給新對象或者新數(shù)組,新對象只是原對象的一個引用,也就是說不管新對象還是原對象,都是引用同一個對象
- 深拷貝:創(chuàng)建一個新的對象或者數(shù)組,將原對象的各項屬性的值拷貝過來,是“值”而不是“引用”,兩者對象是不一樣的
對于概念的解釋,可能也很難理解,下面我們簡單的通過幾個案例向大家介紹!
二、案例實踐
2.1、淺拷貝
首先我們新建兩個對象,其中User關(guān)聯(lián)Account對象,內(nèi)容如下:
public class User {
/**
* 用戶ID
*/
private Long userId;
/**
* 賬戶信息
*/
private Account account;
//...get、set
@Override
public String toString() {
return "User{" +
"userId=" + userId +
", account=" + account +
'}';
}
}
public class Account {
/**
* 賬號余額
*/
private BigDecimal money;
//...get、set
@Override
public String toString() {
return "Account{" +
"money=" + money +
'}';
}
}
使用Spring BeanUtils工具進(jìn)行對象屬性復(fù)制,操作如下:
// 定義某用戶,賬戶余額 100塊
Account sourceAccount = new Account();
sourceAccount.setMoney(BigDecimal.valueOf(100));
User sourceUser = new User();
sourceUser.setUserId(1L);
sourceUser.setAccount(sourceAccount);
// 進(jìn)行對象屬性拷貝
User targetUser = new User();
BeanUtils.copyProperties(sourceUser, targetUser);
System.out.println("修改嵌套對象屬性值前的結(jié)果:" + targetUser.toString());
//修改原始對象賬戶余額為200
sourceAccount.setMoney(BigDecimal.valueOf(200));
System.out.println("修改嵌套對象屬性值后的結(jié)果:" + targetUser.toString());
輸出結(jié)果如下:
修改嵌套對象屬性值前的結(jié)果:User{userId=1, account=Account{money=100}}
修改嵌套對象屬性值后的結(jié)果:User{userId=1, account=Account{money=200}}
從結(jié)果上可以很明顯的得出結(jié)論:當(dāng)修改原始的嵌套對象Account的屬性值時,目標(biāo)對象的Account對象對應(yīng)的值也跟著發(fā)生變化。
很顯然,這與我們預(yù)期想要的對象屬性拷貝是想違背的,我們所期待的結(jié)果是:原始對象值即使發(fā)生變化,目標(biāo)對象的值也不應(yīng)該發(fā)生變化!
面對這種情況,怎么處理呢?
我們可以把對象Account單獨拉出來,進(jìn)行一次屬性值拷貝,然后再進(jìn)行封裝,比如操作如下:
// 定義某用戶,賬戶余額 100塊
Account sourceAccount = new Account();
sourceAccount.setMoney(BigDecimal.valueOf(100));
User sourceUser = new User();
sourceUser.setUserId(1L);
sourceUser.setAccount(sourceAccount);
// 拷貝 Account 對象
Account targetAccount = new Account();
BeanUtils.copyProperties(sourceAccount, targetAccount);
// 拷貝 User 對象
User targetUser = new User();
BeanUtils.copyProperties(sourceUser, targetUser);
targetUser.setAccount(targetAccount);
System.out.println("修改嵌套對象屬性值前的結(jié)果:" + targetUser.toString());
//修改原始對象賬戶余額為200
sourceAccount.setMoney(BigDecimal.valueOf(200));
System.out.println("修改嵌套對象屬性值后的結(jié)果:" + targetUser.toString());
輸出結(jié)果如下:
修改嵌套對象屬性值前的結(jié)果:User{userId=1, account=Account{money=100}}
修改嵌套對象屬性值后的結(jié)果:User{userId=1, account=Account{money=100}}
即使Account對象數(shù)據(jù)發(fā)生變化,也不會改目標(biāo)對象的數(shù)據(jù),與預(yù)期結(jié)果一致!
現(xiàn)在的情況是User只有一個嵌套對象Account,假如像這樣的對象有十幾個呢,采用以上方式顯然不可取。
這個時候深拷貝,該登場了!
2.2、深拷貝
Java 的深拷貝有兩種實現(xiàn)方式,第一種是通過將對象序列化到臨時文件,然后再通過反序列化方式,從臨時文件中讀取數(shù)據(jù),操作案例如下!
首先所有的類,必須實現(xiàn)Serializable接口,推薦顯式定義序列化 ID。
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用戶ID
*/
private Long userId;
/**
* 賬戶信息
*/
private Account account;
//...get、set
@Override
public String toString() {
return "User{" +
"userId=" + userId +
", account=" + account +
'}';
}
}
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 賬號余額
*/
private BigDecimal money;
//...get、set
@Override
public String toString() {
return "Account{" +
"money=" + money +
'}';
}
}
// 定義某用戶,賬戶余額 100塊
Account sourceAccount = new Account();
sourceAccount.setMoney(BigDecimal.valueOf(100));
User sourceUser = new User();
sourceUser.setUserId(1L);
sourceUser.setAccount(sourceAccount);
//把對象寫入文件中
try {
FileOutputStream fos = new FileOutputStream("temp.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(sourceUser);
oos.flush();
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
//從文件中讀取對象
User targetUser = null;
try {
FileInputStream fis = new FileInputStream("temp.out");
ObjectInputStream ois = new ObjectInputStream(fis);
targetUser = (User) ois.readObject();
fis.close();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("修改嵌套對象屬性值前的結(jié)果:" + targetUser.toString());
//修改原始對象賬戶余額為200
sourceAccount.setMoney(BigDecimal.valueOf(200));
System.out.println("修改嵌套對象屬性值后的結(jié)果:" + targetUser.toString());
輸出結(jié)果:
修改嵌套對象屬性值前的結(jié)果:User{userId=1, account=Account{money=100}}
修改嵌套對象屬性值后的結(jié)果:User{userId=1, account=Account{money=100}}
通過序列化和反序列化的方式,可以實現(xiàn)多層復(fù)雜的對象數(shù)據(jù)拷貝。
因為涉及到需要將數(shù)據(jù)寫入臨時磁盤,性能可能會有所下降!
2.3、json 序列化和反序列化
對于對象深度拷貝,還有第二種方式,那就是采用 json 序列化和反序列化相關(guān)的技術(shù)來實現(xiàn),同時性能也比將數(shù)據(jù)寫入臨時磁盤的方式要好很多,并且不需要顯式實現(xiàn)序列化接口。
json 序列化和反序列化的底層思想是,將對象序列化成字符串;然后再將字符串通過反序列化方式成對象。
以jackson工具庫為例,具體使用方式如下!
首先導(dǎo)入相關(guān)的jackson依賴包!
<!--jackson依賴-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
其次,編寫統(tǒng)一Json處理工具類!
public class JsonUtil {
private static final Logger log = LoggerFactory.getLogger(JsonUtil.class);
private static ObjectMapper objectMapper = new ObjectMapper();
static {
// 序列化時,將對象的所有字段全部列入
objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
// 允許沒有引號的字段名
objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
// 自動給字段名加上引號
objectMapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, true);
// 時間默認(rèn)以時間戳格式寫,默認(rèn)時間戳
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
// 忽略空bean轉(zhuǎn)json的錯誤
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// 設(shè)置時間轉(zhuǎn)換所使用的默認(rèn)時區(qū)
objectMapper.setTimeZone(TimeZone.getDefault());
// 反序列化時,忽略在json字符串中存在, 但在java對象中不存在對應(yīng)屬性的情況, 防止錯誤
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
//序列化/反序列化,自定義設(shè)置
SimpleModule module = new SimpleModule();
// 序列化成json時,將所有的long變成string
module.addSerializer(Long.class, ToStringSerializer.instance);
module.addSerializer(Long.TYPE, ToStringSerializer.instance);
// 自定義參數(shù)配置注冊
objectMapper.registerModule(module);
}
/**
* 對象序列化成字符串
* @param obj
* @param <T>
* @return
*/
public static <T> String objToStr(T obj) {
if (null == obj) {
return null;
}
try {
return obj instanceof String ? (String) obj : objectMapper.writeValueAsString(obj);
} catch (Exception e) {
log.warn("objToStr error: ", e);
return null;
}
}
/**
* 字符串反序列化成對象
* @param str
* @param clazz
* @param <T>
* @return
*/
public static <T> T strToObj(String str, Class<T> clazz) {
try {
return clazz.equals(String.class) ? (T) str : objectMapper.readValue(str, clazz);
} catch (Exception e) {
log.warn("strToObj error: ", e);
return null;
}
}
/**
* 字符串反序列化成對象(數(shù)組)
* @param str
* @param typeReference
* @param <T>
* @return
*/
public static <T> T strToObj(String str, TypeReference<T> typeReference) {
try {
return (T) (typeReference.getType().equals(String.class) ? str : objectMapper.readValue(str, typeReference));
} catch (Exception e) {
log.warn("strToObj error", e);
return null;
}
}
}
最后,在相關(guān)的位置引入即可。
// 定義某用戶,賬戶余額 100塊
Account sourceAccount = new Account();
sourceAccount.setMoney(BigDecimal.valueOf(100));
User sourceUser = new User();
sourceUser.setUserId(1L);
sourceUser.setAccount(sourceAccount);
// json序列化、反序列化
User targetUser = JsonUtil.strToObj(JsonUtil.objToStr(sourceUser), User.class);
System.out.println("修改嵌套對象屬性值前的結(jié)果:" + targetUser.toString());
//修改原始對象賬戶余額為200
sourceAccount.setMoney(BigDecimal.valueOf(200));
System.out.println("修改嵌套對象屬性值后的結(jié)果:" + targetUser.toString());
輸出結(jié)果:
修改嵌套對象屬性值前的結(jié)果:User{userId=1, account=Account{money=100}}
修改嵌套對象屬性值后的結(jié)果:User{userId=1, account=Account{money=100}}
與預(yù)期一致!
三、小結(jié)
本文主要圍繞對象的淺拷貝和深拷貝,從使用方面做了一次簡單的內(nèi)容總結(jié)。
淺拷貝下,原對象和目標(biāo)對象,引用都是同一個對象,當(dāng)被引用的對象數(shù)據(jù)發(fā)生變化時,相關(guān)的引用者也會跟著一起變。
深拷貝下,原對象和目標(biāo)對象數(shù)據(jù)是兩個完全獨立的存在,相互直接不受影響。
至于當(dāng)前對象數(shù)據(jù),是應(yīng)該走淺拷貝還是深拷貝模式好,完全取決于當(dāng)前業(yè)務(wù)的需求,沒有絕對的好或者不好!
如果當(dāng)前對象需要深拷貝,推薦采用 json 序列化和反序列化的方式實現(xiàn),相比通過文件寫入的方式進(jìn)行序列化和反序列化,操作簡單且性能高!