談談Java開發中的對象拷貝

在Java開發工作中,有很多時候我們需要將不同的兩個對象實例進行屬性復制,從而基于源對象的屬性信息進行后續操作,而不改變源對象的屬性信息。這兩個對象實例有可能是同一個類的兩個實例,也可能是不同類的兩個實例,但是他們的屬相名稱相同。例如DO、DTO、VO、DAO等,這些實體的意義請查看DDD中分層架構。本文主要介紹幾種對象拷貝的方法

1. 對象拷貝

對象拷貝分為深拷貝和淺拷貝。根據使用場景進行不同選擇。在Java中,數據類型分為值類型(基本數據類型)和引用類型,值類型包括int、double、byte、boolean、char等簡單數據類型,引用類型包括類、接口、數組等復雜類型。

深度拷貝和淺度拷貝的主要區別在于是否支持引用類型的屬性拷貝,本文將探討目前使用較多的幾種對象拷貝的方案,以及其是否支持深拷貝和性能對比。

2. BeanUtils

2.1?apache的BeanUtils方案

使用org.apache.commons.beanutils.BeanUtils進行對象深入復制時候,主要通過向BeanUtils框架注入新的類型轉換器,因為默認情況下,BeanUtils對復雜對象的復制是引用,例如:

public static void beanUtilsTest() throws Exception {// 注冊轉化器BeanUtilsBean.getInstance().getConvertUtils().register(new ArbitrationConvert(), ArbitrationDO.class);Wrapper wrapper = new Wrapper();wrapper.setName("copy");wrapper.setNameDesc("copy complex object!");wrapper.setArbitration(newArbitrationDO());Wrapper dest = new Wrapper();// 對象復制BeanUtils.copyProperties(dest, wrapper);// 屬性驗證wrapper.getArbitration().setBizId("1");System.out.println(wrapper.getArbitration() == dest.getArbitration());System.out.println(wrapper.getArbitration().getBizId().equals(dest.getArbitration().getBizId()));
}public class ArbitrationConvert implements Converter {@Overridepublic <T> T convert(Class<T> type, Object value) {if (ArbitrationDO.class.equals(type)) {try {return type.cast(BeanUtils.cloneBean(value));} catch (Exception e) {e.printStackTrace();}}return null;}
}

可以發現,使用org.apache.commons.beanutils.BeanUtils復制引用時,主和源的引用為同一個,即改變了主的引用屬性會影響到源的引用,所以這是一種淺拷貝。

需要注意的是,apache的BeanUtils中,以下類型如果為空,會報錯(org.apache.commons.beanutils.ConversionException: No value specified for ?*)

/*** Register the converters for other types.* </p>* This method registers the following converters:* <ul>*     <li>Class.class - {@link ClassConverter}*     <li>java.util.Date.class - {@link DateConverter}*     <li>java.util.Calendar.class - {@link CalendarConverter}*     <li>File.class - {@link FileConverter}*     <li>java.sql.Date.class - {@link SqlDateConverter}*     <li>java.sql.Time.class - {@link SqlTimeConverter}*     <li>java.sql.Timestamp.class - {@link SqlTimestampConverter}*     <li>URL.class - {@link URLConverter}* </ul>* @param throwException <code>true if the converters should* throw an exception when a conversion error occurs, otherwise <code>* <code>false if a default value should be used.*/private void registerOther(boolean throwException) {register(Class.class,         throwException ? new ClassConverter()        : new ClassConverter(null));register(java.util.Date.class, throwException ? new DateConverter()        : new DateConverter(null));register(Calendar.class,      throwException ? new CalendarConverter()     : new CalendarConverter(null));register(File.class,          throwException ? new FileConverter()         : new FileConverter(null));register(java.sql.Date.class, throwException ? new SqlDateConverter()      : new SqlDateConverter(null));register(java.sql.Time.class, throwException ? new SqlTimeConverter()      : new SqlTimeConverter(null));register(Timestamp.class,     throwException ? new SqlTimestampConverter() : new SqlTimestampConverter(null));register(URL.class,           throwException ? new URLConverter()          : new URLConverter(null));}

當遇到這種問題是,可以手動將類型轉換器注冊進去,比如data類型:

public class BeanUtilEx extends BeanUtils { private static Map cache = new HashMap(); 
private static Log logger = LogFactory.getFactory().getInstance(BeanUtilEx.class); private BeanUtilEx() { 
} static { 
// 注冊sql.date的轉換器,即允許BeanUtils.copyProperties時的源目標的sql類型的值允許為空 
ConvertUtils.register(new org.apache.commons.beanutils.converters.SqlDateConverter(null), java.sql.Date.class); 
ConvertUtils.register(new org.apache.commons.beanutils.converters.SqlDateConverter(null), java.util.Date.class);  
ConvertUtils.register(new org.apache.commons.beanutils.converters.SqlTimestampConverter(null), java.sql.Timestamp.class); 
// 注冊util.date的轉換器,即允許BeanUtils.copyProperties時的源目標的util類型的值允許為空 
} public static void copyProperties(Object target, Object source) 
throws InvocationTargetException, IllegalAccessException { 
// 支持對日期copy 
org.apache.commons.beanutils.BeanUtils.copyProperties(target, source); } 

2.2 apache的PropertyUtils方案

PropertyUtils的copyProperties()方法幾乎與BeanUtils.copyProperties()相同,主要的區別在于后者提供類型轉換功能,即發現兩個JavaBean的同名屬性為不同類型時,在支持的數據類型范圍內進行轉換,PropertyUtils不支持這個功能,所以說BeanUtils使用更普遍一點,犯錯的風險更低一點。而且它仍然屬于淺拷貝。

Apache提供了 SerializationUtils.clone(T),T對象需要實現 Serializable 接口,他屬于深克隆。

2.3 spring的BeanUtils方案

Spring中的BeanUtils,其中實現的方式很簡單,就是對兩個對象中相同名字的屬性進行簡單get/set,僅檢查屬性的可訪問性。

public static void copyProperties(Object source, Object target) throws BeansException {copyProperties(source, target, (Class)null, (String[])null);}public static void copyProperties(Object source, Object target, Class<?> editable) throws BeansException {copyProperties(source, target, editable, (String[])null);}public static void copyProperties(Object source, Object target, String... ignoreProperties) throws BeansException {copyProperties(source, target, (Class)null, ignoreProperties);}private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties) throws BeansException {Assert.notNull(source, "Source must not be null");Assert.notNull(target, "Target must not be null");Class actualEditable = target.getClass();if(editable != null) {if(!editable.isInstance(target)) {throw new IllegalArgumentException("Target class [" + target.getClass().getName() + "] not assignable to Editable class [" + editable.getName() + "]");}actualEditable = editable;}PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);List ignoreList = ignoreProperties != null?Arrays.asList(ignoreProperties):null;PropertyDescriptor[] var7 = targetPds;int var8 = targetPds.length;for(int var9 = 0; var9 < var8; ++var9) {PropertyDescriptor targetPd = var7[var9];Method writeMethod = targetPd.getWriteMethod();if(writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());if(sourcePd != null) {Method readMethod = sourcePd.getReadMethod();if(readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {try {if(!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {readMethod.setAccessible(true);}Object ex = readMethod.invoke(source, new Object[0]);if(!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {writeMethod.setAccessible(true);}writeMethod.invoke(target, new Object[]{ex});} catch (Throwable var15) {throw new FatalBeanException("Could not copy property \'" + targetPd.getName() + "\' from source to target", var15);}}}}}}

可以看到, 成員變量賦值是基于目標對象的成員列表, 并且會跳過ignore的以及在源對象中不存在的, 所以這個方法是安全的, 不會因為兩個對象之間的結構差異導致錯誤, 但是必須保證同名的兩個成員變量類型相同.

3. dozer

Dozer(http://dozer.sourceforge.net/)能夠實現深拷貝。Dozer是基于反射來實現對象拷貝,反射調用set/get 或者是直接對成員變量賦值?。 該方式通過invoke執行賦值,實現時一般會采用beanutil, Javassist等開源庫。

簡單引用網上的例子,大多都是基于xml的配置,具體請查看其它Blog:

package com.maven.demo;import java.util.HashMap;
import java.util.Map;import org.dozer.DozerBeanMapper;
import org.junit.Test;import static org.junit.Assert.assertEquals;public class Demo{/*** map->bean*/@Testpublic void testDozer1() {Map<String,Object> map = new HashMap();map.put("id", 10000L);map.put("name", "小兵");map.put("description", "帥氣逼人");DozerBeanMapper mapper = new DozerBeanMapper();ProductVO product = mapper.map(map, ProductVO.class);assertEquals("小兵",product.getName());assertEquals("帥氣逼人",product.getDescription());assertEquals(Long.valueOf("10000"), product.getId());}/*** VO --> Entity  (不同的實體之間,不同的屬性字段進行復制)*/@Testpublic void testDozer2(){ProductVO product = new ProductVO();product.setId(10001L);product.setName("xiaobing");product.setDescription("酷斃了");DozerBeanMapper mapper = new DozerBeanMapper();ProductEntity productEntity = mapper.map(product, ProductEntity.class);assertEquals("xiaobing",productEntity.getProductName());}}

4. ?MapStrcut

MapStrcut屬于編譯期的對象復制方案,它能夠動態生成set/get代碼的class文件?,在運行時直接調用該class文件。該方式實際上扔會存在set/get代碼,只是不需要自己寫了。

@Mapper(componentModel = "spring")
public interface MonitorAppGroupIdcDTOMapper {MonitorAppGroupIdcDTOMapper MAPPER = Mappers.getMapper(MonitorAppGroupIdcDTOMapper.class);void mapping(MonitorAppGroupIdcDTO source, @MappingTarget MonitorAppGroupIdcDTO dest);
}

5. 自定義Pojoconvert

public J copyPojo( P src, J des) throws NoSuchMethodException,SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {if(src == null || des==null){return null;}String name = null ;String sufix = null;Class<?> cls = des.getClass() ;Method[] methods = cls.getMethods();for(Method m: methods){name = m.getName();if(name!=null && name.startsWith("set") && m.getParameterTypes().length==1){sufix = name.substring(3);m.getParameterTypes() ;Method getM = cls.getMethod("get"+sufix);m.invoke(des, getM.invoke(src));}}return des ;
}

沒有那么多驗證,不是很安全但是性能不錯。

6.?BeanCopier

@Testpublic void test_convert_entity_to_model_performance_use_beancopier(){List<ShopCouponEntity> entityList  = ...long start = System.currentTimeMillis();BeanCopier b = BeanCopier.create(ShopCouponEntity.class, ShopCouponModel.class, false);List<ShopCouponModel> modelList = new ArrayList<>();for (ShopCouponEntity src : entityList) {ShopCouponModel dest = new ShopCouponModel();b.copy(src, dest, null);modelList.add(dest);}System.out.printf("BeanCopier took time: %d(ms)%n",System.currentTimeMillis() - start);}
可以通過緩存BeanCopier的實例來提高性能。

BeanCopier b = getFromCache(sourceClass,targetClass); //從緩存中取long start = System.currentTimeMillis();List<ShopCouponModel> modelList = new ArrayList<>();for (ShopCouponEntity src : entityList) {ShopCouponModel dest = new ShopCouponModel();b.copy(src, dest, null);modelList.add(dest);}

7.?fastjson和GSON

使用fastjson和GSON主要是通過對象json序列化和反序列化來完成對象復制,這里只是提供一種不一樣的對象拷貝的思路,例子略。

8. 性能

對兩種BeanUtils、Gson以及自定義Pojoconvert測試了性能

NewNovelMode des = null ;
NewNovelMode ori = buildModel();
Gson gson = new Gson();     
int count = 100000;
//org.springframework.beans.BeanUtils.copyProperties
long s = System.currentTimeMillis();
for(int i=0;i<count;i++){des = new NewNovelMode();org.springframework.beans.BeanUtils.copyProperties(ori, des);
}
System.out.println("springframework BeanUtils cost:"+(System.currentTimeMillis() - s));
//      System.out.println(new Gson().toJson(des));//org.apache.commons.beanutils.BeanUtils
s = System.currentTimeMillis();
for(int i=0;i<count;i++){des = new NewNovelMode();org.apache.commons.beanutils.BeanUtils.copyProperties(des, ori);
}
System.out.println("apache BeanUtils cost:"+(System.currentTimeMillis() - s));
//      System.out.println(new Gson().toJson(des));//gson轉換
s = System.currentTimeMillis();
for(int i=0;i<count;i++){des = gson.fromJson(gson.toJson(ori), NewNovelMode.class);
}
System.out.println("gson cost:"+(System.currentTimeMillis() - s));
//      System.out.println(new Gson().toJson(des));//Pojo轉換類
s = System.currentTimeMillis();
PojoUtils<NewNovelMode, NewNovelMode> pojoUtils = new PojoUtils<NewNovelMode, NewNovelMode>();
for(int i=0;i<count;i++){des = new NewNovelMode();pojoUtils.copyPojo(ori,des);
}
System.out.println("Pojoconvert cost:"+(System.currentTimeMillis() - s));
//      System.out.println(new Gson().toJson(des));

結果就不貼出來了,在這里總結一下

Spring的BeanUtils比較穩定,不會因為量大了,耗時明顯增加,但其實基準耗時比較長;apache的BeanUtils穩定性與效率都不行,不可取;Gson,因為做兩個gson轉換,所以正常項目中,可能耗時會更少一些;PojoUtils穩定不如spring,但是總耗時優勢明顯,原因是它只是根據項目的需求,實現的簡單的轉換模板,這個代碼在其它的幾個工具類均有。

而在網上的其他Blog中(參見Reference),對Apache的BeanUtils、PropertyUtils和CGLIB的BeanCopier作了性能測試。

測試結果:

性能對比: BeanCopier > BeanUtils. 其中BeanCopier的性能高出另外兩個100數量級。

綜上推薦使用:

1.?BeanUtils(簡單,易用)

2.?BeanCopier(加入緩存后和手工set的性能接近)

3. Dozer(深拷貝)

4. fastjson(特定場景下使用)


轉自:https://my.oschina.net/hosee/blog/1483965


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/443322.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/443322.shtml
英文地址,請注明出處:http://en.pswp.cn/news/443322.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

gitmaven命令

git命令 git diff #查看差異 git push origin feature/recover_pwd_bug #推送 git commit -m ‘perf #重置密碼邏輯優化 git log #查看提交版本號 git reset --hard <版本號> #本地回退到相應的版本 git push origin <分支名> --force #遠端的倉庫也回退到相應…

【算法系列之一】二叉樹最小深度

題目&#xff1a; 給定一個二叉樹&#xff0c;找出其最小深度。 最小深度是從根節點到最近葉子節點的最短路徑上的節點數量。 說明: 葉子節點是指沒有子節點的節點。 示例: 給定二叉樹 [3,9,20,null,null,15,7], 3/ \9 20/ \15 7 返回它的最小深度 2. 答案&#xf…

【算法系列之二】反波蘭式

問題&#xff1a; 用反波蘭式表示算術表達式的值。 有效運算符是,-,*,/。每個操作數可以是一個整數或另一個表達式。 一些例子&#xff1a; ["2", "1", "", "3", "*"] -> ((2 1) * 3) -> 9["4", "13…

【算法系列之三】單鏈表反轉

問題&#xff1a; 實現單鏈表反轉 答案&#xff1a; 鏈表準備 class Node {private int Data;// 數據域private Node Next;// 指針域public Node(int Data) {// super();this.Data Data;}public int getData() {return Data;}public void setData(int Data) {this.Data D…

Java常見異常總結

1、java.lang.NullPointerException(空指針異常)   調用了未經初始化的對象或者是不存在的對象 經常出現在創建圖片&#xff0c;調用數組這些操作中&#xff0c;比如圖片未經初始化&#xff0c;或者圖片創建時的路徑錯誤等等。對數組操作中出現空指針&#xff0c; 即把數組的…

從數據庫表中隨機獲取N條記錄的SQL語句

Oracle: select * from (select * from tableName order by dbms_random.value) where rownum < N; MS SQLServer: select top N * from tableName order by newid(); My SQL: select * from tableName order by rand() limit N; 轉自&#xff1a;http://blog.csdn.net/sent…

Linux下的MySQL安裝及卸載

1.1 查看mysql的安裝路徑&#xff1a; [rootbogon ~]# whereis mysql mysql: /usr/bin/mysql /usr/lib/mysql/usr/share/mysql /usr/share/man/man1/mysql.1.gz 1.2 查看mysql的安裝包&#xff1a; [rootbogon ~]# rpm -qa|grep mysql mysql-community-client-5.6.26-2.…

mysql explain用法

explain顯示了mysql如何使用索引來處理select語句以及連接表。可以幫助選擇更好的索引和寫出更優化的查詢語句。使用方法&#xff0c;在select語句前加上explain就可以了&#xff0c;如&#xff1a;explain select * from statuses_status where id11;創建測試表&#xff1a;CR…

Linux 性能檢查命令總結

如果你的Linux服務器突然負載暴增&#xff0c;告警短信快發爆你的手機&#xff0c;如何在最短時間內找出Linux性能問題所在&#xff1f;

線程池的各種使用場景

&#xff08;1&#xff09;高并發、任務執行時間短的業務&#xff0c;線程池線程數可以設置為CPU核數1&#xff0c;減少線程上下文的切換 &#xff08;2&#xff09;并發不高、任務執行時間長的業務要區分開看&#xff1a; a&#xff09;假如是業務時間長集中在IO操作上…

Java線程面試題 Top 50

不管你是新程序員還是老手&#xff0c;你一定在面試中遇到過有關線程的問題。Java語言一個重要的特點就是內置了對并發的支持&#xff0c;讓Java大受企業和程序員的歡迎。大多數待遇豐厚的Java開發職位都要求開發者精通多線程技術并且有豐富的Java程序開發、調試、優化經驗&…

深入理解Semaphore

使用 Semaphore是計數信號量。Semaphore管理一系列許可證。每個acquire方法阻塞&#xff0c;直到有一個許可證可以獲得然后拿走一個許可證&#xff1b;每個release方法增加一個許可證&#xff0c;這可能會釋放一個阻塞的acquire方法。然而&#xff0c;其實并沒有實際的許可證這…

【算法系列之四】柱狀圖儲水

題目&#xff1a; 給定一個數組&#xff0c;每個位置的值代表一個高度&#xff0c;那么整個數組可以看做是一個直方圖&#xff0c; 如果把這個直方圖當作容器的話&#xff0c;求這個容器能裝多少水 例如&#xff1a;3&#xff0c;1&#xff0c;2&#xff0c;4 代表第一個位…

鹽城大數據產業園人才公寓_岳西大數據產業園規劃設計暨建筑設計方案公布,搶先一睹效果圖...

近日&#xff0c;岳西縣大數據產業園規劃設計暨建筑設計方案公布。岳西縣大數據產業園項目總占地面積17014.10㎡(約合25.52畝)&#xff0c;擬建總建筑面積約為61590.84㎡(地上建筑面積39907.49㎡&#xff0c;地下建筑面積21602.35㎡)。以“科技圓環”為主題&#xff0c;組建出一…

【算法系列之五】對稱二叉樹

給定一個二叉樹&#xff0c;檢查它是否是鏡像對稱的。 例如&#xff0c;二叉樹 [1,2,2,3,4,4,3] 是對稱的。 1/ \2 2/ \ / \ 3 4 4 3但是下面這個 [1,2,2,null,3,null,3] 則不是鏡像對稱的: 1/ \2 2\ \3 3 說明: 如果你可以運用遞歸和迭代兩種方法解決這個問題&a…

【算法系列之六】兩整數之和

不使用運算符 和 - &#xff0c;計算兩整數 a 、b 之和。 示例 1: 輸入: a 1, b 2 輸出: 3示例 2: 輸入: a -2, b 3 輸出: 1 方法一&#xff1a;遞歸 public static int getSum1(int a, int b) {if ((a & b) ! 0) { // 判斷是否有進位return getSum1(a ^ b, (a &…

cuda默認函數與c++沖突_好程序員Python教程系列-第8講:函數和模塊

好程序員Python教程系列-第8講&#xff1a;函數和模塊&#xff0c;在講解本章節的內容之前&#xff0c;我們先來研究一道數學題&#xff0c;請說出下面的方程有多少組正整數解。事實上&#xff0c;上面的問題等同于將8個蘋果分成四組每組至少一個蘋果有多少種方案&#xff0c;所…

【算法系列之七】合并兩個有序鏈表

將兩個有序鏈表合并為一個新的有序鏈表并返回。新鏈表是通過拼接給定的兩個鏈表的所有節點組成的。 示例&#xff1a; 輸入&#xff1a;1->2->4, 1->3->4 輸出&#xff1a;1->1->2->3->4->4/*** Definition for singly-linked list.* public cla…

mfc讓圖片與按鈕一起_對許多張圖片進行批量裁剪,看看我是如何快速做到的

概要&#xff1a;當我們需要對很多圖片進行批量裁剪時&#xff0c;以往的辦法是自己一張一張圖片去操作&#xff0c;非常麻煩。有沒有這樣一個工具&#xff0c;能夠幫我們批量進行處理呢&#xff1f;之前小編在網上找了非常多的軟件&#xff0c;一個一個地安裝試用&#xff0c;…

【算法系列之八】刪除鏈表的倒數第N個節點

給定一個鏈表&#xff0c;刪除鏈表的倒數第 n 個節點&#xff0c;并且返回鏈表的頭結點。 示例&#xff1a; 給定一個鏈表: 1->2->3->4->5, 和 n 2.當刪除了倒數第二個節點后&#xff0c;鏈表變為 1->2->3->5.說明&#xff1a; 給定的 n 保證是有效的…