Java?對象深拷貝工具類的實現(xiàn)
1. 使用場景
我們在Java編碼中,有時候可能會經(jīng)常遇到對象拷貝的場景。
1.1 場景一
當(dāng)我們更新一個對象的時候,如果要記錄對象屬性的前后變化,那么在更新對象之前,我們應(yīng)該首先將對象拷貝暫存起來,且這個時候的拷貝一定是深拷貝(內(nèi)存地址不同的兩個對象),因為Java存在對象引用,將一個對象賦值給另外一個對象,他是淺拷貝的(兩個不同變量名,但實際內(nèi)存地址一樣的兩個對象)的話,也就是說當(dāng)我們?nèi)ジ峦瓿蓪傩灾档臅r候,其實是設(shè)置的同一個對象,那么這個時候就會導(dǎo)致更新前后無變化的情況。
1.2 場景二
又比如,當(dāng)我們從數(shù)據(jù)庫中查詢出一個實體對象的時候,這個對象往往對應(yīng)的是和數(shù)據(jù)庫字段 一 一 對應(yīng)的實體,但這個實體往往又不會滿足我們的頁面需求。比如我們查詢學(xué)生課程表的時候,我們數(shù)據(jù)庫往往只是存的一個 id 對應(yīng)關(guān)系,但頁面往往是展示的名稱,那么這個名稱字段我們的表對應(yīng)的實體類是不應(yīng)該存在的,這個時候,我們應(yīng)該創(chuàng)建一個對應(yīng)的 VO (View Object)類,把額外的需要字段定義在這里面,同時可以去繼承原表實體類,這樣的一個對象就滿足了。到時候,我們把原表實體對應(yīng)的字段值拷貝到 VO 對象中后,再設(shè)置其他表額外的字段,這樣就可以返回給前端頁面進(jìn)行展示了。
綜上:所以,對象拷貝還是挺有用途的,但如果我們拷貝對象的時候,去一個一個字段挨著進(jìn)行取值拷貝的話,難免代碼看上去不夠優(yōu)雅。于是,搞一個對象拷貝工具類還是很有必要的。
2. Spring 中的對象拷貝
其實,在 Spring 中,也有類似的拷貝方法。他就是位于 org.springframework.beans.BeanUtils 工具類中的 copyProperties 方法。下面就簡單演示下這個方法的使用效果。
為了方便演示,我們創(chuàng)建兩個有部分相同屬性的對象 Cat 類和 Dog 類(都有 name 和 age 字段)。
Cat 類如下:
@Data
public class Cat {
private String name;
private Integer age;
private String color;
}Dog 類如下:
@Data
public class Dog {
private String name;
private Integer age;
private String address;
}測試代碼:
import org.springframework.beans.BeanUtils;
public class Test {
public static void main(String[] args) {
// 實例化一個 Cat 對象并賦值屬性
Cat cat = new Cat();
cat.setName("tom");
cat.setAge(5);
// 實例化一個 Dog 對象
Dog dog = new Dog();
// 將 cat 對象中的屬性值拷貝至 dog 中
BeanUtils.copyProperties(cat, dog);
System.out.println("拷貝后:" + dog);
}
}測試效果:

可以看到,相同的 name 和 age 已經(jīng)復(fù)制過去了。
3. 本工具類中的對象拷貝
上面我們演示了 Spring 下 BeanUtils 工具類中的對象屬性拷貝,雖然他也可以成功拷貝對象中的屬性,但對于我個人來說,還是有點不適應(yīng)。
首先,Spring 去拷貝一個對象屬性的時候,需要先創(chuàng)建好另外一個對象,然后再進(jìn)行屬性拷貝,這一步對象創(chuàng)建是明顯可以放到工具方法中去的。
其次,如果只是本類復(fù)制的話,參數(shù)只需要傳一個源對象的實例就應(yīng)該夠了,而Spring就算拷貝本類,也得傳兩個參數(shù),即源實例對象和目標(biāo)實例對象。
另外,Spring 的對象拷貝不支持批量拷貝,比如我們將 List<Cat> 屬性拷貝后,生成一個 List<Dog> 中,只能自己循環(huán)去拷貝生成每個 Dog,然后添加到 List<Dog> 中。
于是,敝人針對個人習(xí)慣,編寫了一個適合自己的編碼習(xí)慣的對象拷貝工具類 BeanUtils(類名還是參照的 Spring),具體使用效果如下。
下面先做效果演示,工具類源碼放在文章最后。
3.1 拷貝對象本身(單個)
比如,我們想復(fù)制一個對象本身(如 cat),那么直接使用下面這個方法就可以了。
Cat newCat = BeanUtils.copy(cat);
測試代碼:

測試效果:

從測試結(jié)果我們可以看到,源對象和復(fù)制對象的每個字段值已經(jīng)拷貝過去了,但兩個對象的內(nèi)存 hashCode 并不相同,說明并不是同一個對象,也就說我們是進(jìn)行深拷貝的,兩個對象是互不影響的。
另外,我們這個工具類不但支持類對象本身屬性拷貝,連父類屬性拷貝也是支持的。
比如,Cat類去繼承下面這個 Animal 類:
@Data
public class Animal {
private Integer price;
private Date birth;
}@Data
public class Cat extends Animal {
private String name;
private Integer age;
private String color;
}我們再試試測試一下:

測試效果:

可以看到,我們的父類屬性字段值也確實復(fù)制成功了。
3.2 拷貝對象本身(批量)
工具類中不僅支對單個對象拷貝的,對多個對象的拷貝也是支持的。
List<Cat> newCatList = BeanUtils.copyList(catList);
測試代碼:

測試效果:

可以看到,批量屬性復(fù)制也是OK的,拷貝后的集合中每個對象新生成的深拷貝對象。
3.3 拷貝對象屬性至其他類(單個)
上面,我們演示了對象本身復(fù)制的效果,下面繼續(xù)演示下拷貝同名字段到其他屬性的效果。
Dog dog = BeanUtils.copy(cat, Dog.class);
我們把 Cat 中的同名字段屬性拷貝到 Dog 中去,我們讓 Dog 也去繼承下 Anima 類。
@Data
public class Dog extends Animal {
private String name;
private Integer age;
private String address;
}測試代碼:
因為拷貝前后是兩個完全不一樣的對象了,所以這里就不再打印地址 hashCode 來進(jìn)行說明是深拷貝了。

測試效果:
可以看到 cat 中的所有相同屬性已經(jīng)拷貝到 dog 中去了。

3.4 拷貝對象屬性至其他類(批量)
同理,我們拷貝對象屬性至其他類也是支持批量操作的。
測試代碼:

測試效果:
可以看到,批量復(fù)制也是OK的。

至此,整個對象的拷貝的四個常用方法已經(jīng)都已經(jīng)支持了。
4. 工具類源碼
下面就是整個工具類的源碼 BeanUtils :
package com.zyq.utils.common;
import java.lang.reflect.Field;
import java.util.*;
/**
* @author zyqok
* @since 2022/07/18
*/
@SuppressWarnings("unused")
public class BeanUtils {
/**
* 拷貝數(shù)據(jù)到新對象(單個)
*
* @param source 源實例對象
* @return 拷貝后的新實例對象
*/
public static <T> T copy(T source) {
if (Objects.isNull(source)) {
return null;
}
Class<?> c = source.getClass();
List<Field> fields = getFields(c);
return newInstance(source, c, fields);
}
/**
* 拷貝數(shù)據(jù)到新對象(批量)
*
* @param sourceList 源實例對象集合
* @return 拷貝后的新實例對象集合
*/
public static <T> List<T> copyList(List<T> sourceList) {
if (Objects.isNull(sourceList) || sourceList.isEmpty()) {
return Collections.emptyList();
}
Class<?> c = getClass(sourceList);
if (Objects.isNull(c)) {
return Collections.emptyList();
}
List<Field> fields = getFields(c);
List<T> ts = new ArrayList<>();
for (T t : sourceList) {
T s = newInstance(t, c, fields);
if (Objects.nonNull(s)) {
ts.add(s);
}
}
return ts;
}
/**
* 單個深度拷貝
*
* @param source 源實例化對象
* @param target 目標(biāo)對象類(如:User.class)
* @return 目標(biāo)實例化對象
*/
public static <T> T copy(Object source, Class<T> target) {
if (Objects.isNull(source) || Objects.isNull(target)) {
return null;
}
List<Field> sourceFields = getFields(source.getClass());
List<Field> targetFields = getFields(target);
T t = null;
try {
t = newInstance(source, target, sourceFields, targetFields);
} catch (Exception e) {
e.printStackTrace();
}
return t;
}
/**
* 批量深度拷貝(如果原集合中有null,則自動忽略)
*
* @param sourceList 源實例化對象集合
* @param target 目標(biāo)對象類(如:User.class)
* @return 目標(biāo)實例化對象集合
*/
public static <T, K> List<K> copyList(List<T> sourceList, Class<K> target) {
if (Objects.isNull(sourceList) || sourceList.isEmpty() || Objects.isNull(target)) {
return Collections.emptyList();
}
Class<?> c = getClass(sourceList);
if (Objects.isNull(c)) {
return Collections.emptyList();
}
List<Field> sourceFields = getFields(c);
List<Field> targetFields = getFields(target);
List<K> ks = new ArrayList<>();
for (T t : sourceList) {
if (Objects.nonNull(t)) {
try {
K k = newInstance(t, target, sourceFields, targetFields);
ks.add(k);
} catch (Exception e) {
e.printStackTrace();
}
}
}
return ks;
}
/**
* 獲取List集合中的類名
*
* @param list 對象集合
* @return 類名
*/
private static <T> Class<?> getClass(List<T> list) {
for (T t : list) {
if (Objects.nonNull(t)) {
return t.getClass();
}
}
return null;
}
/**
* 實例化同源對象
*
* @param source 源對象
* @param c 源對象類名
* @param fields 源對象屬性集合
* @return 同源新對象
*/
@SuppressWarnings("unchecked")
private static <T> T newInstance(T source, Class<?> c, List<Field> fields) {
T t = null;
try {
t = (T) c.newInstance();
for (Field field : fields) {
field.setAccessible(true);
field.set(t, field.get(source));
}
} catch (Exception e) {
e.printStackTrace();
}
return t;
}
/**
* 目標(biāo)實例化對象
*
* @param source 原對實例化象
* @param target 目標(biāo)對象類
* @param sourceFields 源對象字段集合
* @param targetFields 目標(biāo)對象屬性字段集合
* @return 目標(biāo)實例化對象
*/
private static <T> T newInstance(Object source, Class<T> target, List<Field> sourceFields,
List<Field> targetFields) throws Exception {
T t = target.newInstance();
if (targetFields.isEmpty()) {
return t;
}
for (Field field : sourceFields) {
field.setAccessible(true);
Object o = field.get(source);
Field sameField = getSameField(field, targetFields);
if (Objects.nonNull(sameField)) {
sameField.setAccessible(true);
sameField.set(t, o);
}
}
return t;
}
/**
* 獲取目標(biāo)對象中同源對象屬性相同的屬性(字段名稱,字段類型一致則判定為相同)
*
* @param field 源對象屬性
* @param fields 目標(biāo)對象屬性集合
* @return 目標(biāo)對象相同的屬性
*/
private static Field getSameField(Field field, List<Field> fields) {
String name = field.getName();
String type = field.getType().getName();
for (Field f : fields) {
if (name.equals(f.getName()) && type.equals(f.getType().getName())) {
return f;
}
}
return null;
}
/**
* 獲取一個類中的所有屬性(包括父類屬性)
*
* @param c 類名
* @return List<Field>
*/
private static List<Field> getFields(Class<?> c) {
List<Field> fieldList = new ArrayList<>();
Field[] fields = c.getDeclaredFields();
if (fields.length > 0) {
fieldList.addAll(Arrays.asList(fields));
}
return getSuperClassFields(c, fieldList);
}
/**
* 遞歸獲取父類屬性
*
* @param o 類名
* @param allFields 外層定義的所有屬性集合
* @return 父類所有屬性
*/
private static List<Field> getSuperClassFields(Class<?> o, List<Field> allFields) {
Class<?> superclass = o.getSuperclass();
if (Objects.isNull(superclass) || Object.class.getName().equals(superclass.getName())) {
return allFields;
}
Field[] fields = superclass.getDeclaredFields();
if (fields.length == 0) {
return allFields;
}
allFields.addAll(Arrays.asList(fields));
return getSuperClassFields(superclass, allFields);
}
}到此這篇關(guān)于Java 對象深拷貝工具類的實現(xiàn)的文章就介紹到這了,更多相關(guān)Java 對象深拷貝工具類的實現(xiàn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring緩存注解@Cacheable @CacheEvit @CachePut使用介紹
Spring在3.1版本,就提供了一條基于注解的緩存策略,實際使用起來還是很絲滑的,本文將針對幾個常用的注解進(jìn)行簡單的介紹說明,有需要的小伙伴可以嘗試一下2021-07-07
解決Java中SimpleDateFormat線程不安全的五種方案
SimpleDateFormat 就是一個典型的線程不安全事例,本文主要介紹了解決Java中SimpleDateFormat線程不安全的五種方案,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05
基于<aop:aspect>與<aop:advisor>的區(qū)別

