Java对象复制
文章目錄
- 前言
- 何不可變類
- 對象復(fù)制方式
- 1.直接賦值
- 2.淺拷貝
- 3.深拷貝
- 對象復(fù)制方案
- 1.get/set
- 2.Spring BeanUtils
- 3.Apache BeanUtils
- 4.BeanCopier
- 5.Orika
- 6.Dozer
- 7.MapStruct
- 8.Bean Mapping
- 9.Bean Mapping ASM
- 10.ModelMapper
- 11.JMapper
- 12.Json2Json
- 復(fù)制方案選擇
前言
在我們實際項目開發(fā)過程中,我們經(jīng)常需要將不同的兩個對象實例進行屬性復(fù)制,從而基于源對象的屬性信息進行后續(xù)操作,而不改變源對象的屬性信息。比如DTO數(shù)據(jù)傳輸對象和數(shù)據(jù)對象DO,我們需要將DO對象進行屬性復(fù)制到DTO,但是對象格式又不一樣,所以我們需要編寫映射代碼將對象中的屬性值從一種類型轉(zhuǎn)換成另一種類型。
這種轉(zhuǎn)換最原始的方式就是手動編寫大量的get/set代碼,屬性少的時候還好,屬性多的時候就非常繁瑣了,一個合格的程序員顯然不會僅僅局限于get/set。
針對這個問題,市場上誕生了很多方便的類庫,用于對象拷貝。常用的有apache BeanUtils、spring BeanUtils、Dozer、Orika等拷貝工具。
何不可變類
當(dāng)類的實例一經(jīng)創(chuàng)建,其內(nèi)容便不可改變,即無法修改其成員變量。
Java中有一些特殊的類是不可變類,八個基本類型的包裝類和String類都屬于不可變類。
不可變類的特殊性:
有兩個不可變類對象,當(dāng)兩個對象指向同一引用時,修改某一對象的值,不會對另一個對象造成影響。
下面舉例說明
-
源碼:
public static void main(String[] args) {Integer a = 1;String aa = "1";Integer b = a;String bb = aa;System.out.println("a修改前b:"+b);System.out.println("aa修改前bb:"+bb);a = 2;aa = "2";System.out.println("a修改后b:"+b);System.out.println("aa修改后bb:"+bb); } -
debug:
-
日志:
a修改前b:1 aa修改前bb:1 a修改后b:1 aa修改后bb:1
由此可見,a和aa的修改不會影響b和bb的值。
方法參數(shù)的傳遞:
- 基本類型傳遞的是值;
- 引用類型傳遞的是對象的引用。
- 不可變類型,在方法里修改了對象的值,也不會影響到原對象(不可變性);
- 可變類型,在方法里修改了對象的值,原對象相應(yīng)的值也會變動。
對象復(fù)制方式
對象復(fù)制有三種方式:直接賦值、淺拷貝、深拷貝。
1.直接賦值
- 基本數(shù)據(jù)類型復(fù)制的是值;
- 引用數(shù)據(jù)類型復(fù)制的是對象的引用,原始對象及目標(biāo)對象引用的是同一個對象。
2.淺拷貝
創(chuàng)建一個新對象,然后將當(dāng)前對象的非靜態(tài)字段復(fù)制到該新對象。
- 基本數(shù)據(jù)類型復(fù)制的是值;
- 引用數(shù)據(jù)類型復(fù)制的是對象的引用(不可變類型特殊)。
注意:String類型、Integer等基本數(shù)據(jù)類型的包裝類型,因為時不可變類型,所以即使進行的是淺拷貝,原始對象的改變并不會影響目標(biāo)對象。
3.深拷貝
創(chuàng)建一個新對象,然后將當(dāng)前對象的非靜態(tài)字段復(fù)制到該新對象。
- 無論該字段是基本類型的還是引用類型,都復(fù)制獨立的一份。當(dāng)你修改其中一個對象的任何內(nèi)容時,都不會影響另一個對象的內(nèi)容。
對象復(fù)制方案
一個好用的屬性復(fù)制方案,需要有哪些特性:
市場上的對象轉(zhuǎn)換方案主要分類:
- 直接編寫get、set代碼(硬編碼);
- 通過反射實現(xiàn);
- 編譯期生成get、set代碼;
- 基于AOP、ASM、CGlib等技術(shù)實現(xiàn)。
12種對象轉(zhuǎn)換方案歸納:
| get/set | ★★★☆☆ | ★★★★★ | 手寫get、set | 日常使用最多,性能好,只是較麻煩,需要手寫。 |
| Spring BeanUtils | ★★★☆☆ | ★★★★☆ | 基于反射 | 日常使用較多,性能較好,推薦使用 |
| Apache BeanUtils | ☆☆☆☆☆ | ★☆☆☆☆ | 基于反射 | 兼容性較差,性能差,不推薦使用 |
| BeanCopier | ★★★☆☆ | ★★★★☆ | 基于CGlib | 性能較好,使用也不復(fù)雜,可以使用 |
| Orika | ★★☆☆☆ | ★★★☆☆ | 基于Javasisst字節(jié)碼增強 | 性能不太突出 |
| Dozer | ★☆☆☆☆ | ★★☆☆☆ | 基于反射的屬性映射(遞歸映射) | 性能較差,不太推薦使用 |
| MapStruct | ★★★★★ | ★★★★★ | 編譯期生成get、set | 性能好,結(jié)合到框架中使用方便,推薦使用 |
| Bean Mapping | ★★☆☆☆ | ★★★☆☆ | 基于反射 | 性能一般,不太推薦使用 |
| Bean Mapping ASM | ★★★☆☆ | ★★★★☆ | 基于ASM字節(jié)碼增強 | 性能較好,但暫時不夠靈活,可以使用 |
| ModelMapper | ★★★☆☆ | ★★★☆☆ | 基于反射 | 性能一般,不太推薦使用 |
| JMapper | ★★★★☆ | ★★★★★ | 映射器方式實現(xiàn) | 性能較好,使用略微麻煩,可以使用 |
| Json2Json | ☆☆☆☆☆ | ★☆☆☆☆ | 基于JSON序列化和反序列化 | 野路子,性能較差,不推薦使用 |
分別測試這12種屬性轉(zhuǎn)換操作分別在一百次、一千次、一萬次、十萬次、一百萬次時候的性能時間對比。
- BeanUtils.copyProperties是大家代碼里最常出現(xiàn)的工具類,但只要你不把它用錯成Apache包下的,而是使用Spring提供的,就基本還不會對性能造成多大影響。
- 但如果說性能更好,可替代手動get、set的,還是MapStruct更好用,因為它本身就是在編譯期生成get、set代碼,和我們寫get、set一樣。
- 其他一些組件包主要基于AOP、ASM、CGlib等技術(shù)手段實現(xiàn)的,所以也會有相應(yīng)的性能損耗。
1.get/set
直接手寫get/set方法實現(xiàn)數(shù)據(jù)的復(fù)制。
這種方式也是日常使用的最多的,性能較好,就是操作起來有點麻煩。尤其是當(dāng)對象屬性較多的時候。
減少手寫代碼的方式:
2.Spring BeanUtils
同樣是基于反射的屬性拷貝(Introspector機制獲取到類的屬性來進行賦值操作)。Spring 提供的copyProperties要比Apache好用得多,這也是大家用得比較多的一種復(fù)制方式。
Spring BeanUtils的實現(xiàn)方式非常簡單,就是對兩個對象中相同名字的屬性進行簡單的get/set,僅檢查屬性的可訪問性。成員變量賦值是基于目標(biāo)對象的成員列表,并且會跳過ignoreProperties的以及在源對象中不存在,不會因為兩個對象之間的結(jié)構(gòu)差異導(dǎo)致錯誤,但是必須保證同名的兩個成員變量類型相同。
Introspector:
是一個專門處理bean的工具類,用來獲取Bean體系里的propertiesDescriptor、methodDescriptor利用反射獲取Method信息,是反射的上層。只進行一次反射解析,通過WeakReference靜態(tài)類級別緩存Method,在jvm不夠時會被回收。
特點:
- 字段名不一致,屬性無法復(fù)制;
- 類型不一致,屬性無法復(fù)制。但是注意,如果類型為基本類型以及基本類型的包裝類,這種可以轉(zhuǎn)化;
- 淺拷貝。
依賴:
<dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId><version>5.2.8.RELEASE</version> </dependency>方法:
/*** source:源對象* target:目標(biāo)對象* editable:目標(biāo)對象類的Class對象(需復(fù)制的屬性基于該Class,當(dāng)該值為null時需復(fù)制的屬性基于目標(biāo)對象的Class)* ignoreProperties:需忽略的屬性列表*/ BeanUtils.copyProperties(Object source, Object target)BeanUtils.copyProperties(Object source, Object target, Class<?> editable)BeanUtils.copyProperties(Object source, Object target, String... ignoreProperties)實現(xiàn):
public static void main(String[] args) {UserDTO userDTO = new UserDTO();userDTO.setId(1);userDTO.setUserName("哈哈");userDTO.setCreateTime(new Date());UserVO userVO = new UserVO();BeanUtils.copyProperties(userDTO, userVO);System.out.println(JSON.toJSONString(userVO)); }3.Apache BeanUtils
推薦:☆☆☆☆☆
性能:★☆☆☆☆
手段:Introspector機制獲取到類的屬性來進行賦值操作
點評:兼容性交差,效率較低,不建議使用
Apache BeanUtils使用起來很方便,不過其底層源碼為了追求完美,加了過多的包裝,使用了很多反射,做了很多校驗,做了類型的轉(zhuǎn)換,甚至還會檢驗對象所屬的類的可訪問性,可謂相當(dāng)復(fù)雜,過度的追求完美反而導(dǎo)致兼容性變差,也導(dǎo)致了性能較低,所以阿里巴巴開發(fā)手冊上強制避免使用Apache BeanUtils。
特點:
- 字段名不一致的屬性無法被復(fù)制;
- 類型不一致的字段,將會進行默認(rèn)類型轉(zhuǎn)化;
- 淺拷貝。
依賴:
<dependency><groupId>commons-beanutils</groupId><artifactId>commons-beanutils</artifactId><version>1.9.4</version> </dependency>方法:
/*** dest:目標(biāo)對象* orig:源對象*/ BeanUtils.copyProperties(Object dest, Object orig)實現(xiàn):
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {UserDTO userDTO = new UserDTO();userDTO.setId(1);userDTO.setUserName("哈哈");userDTO.setCreateTime(new Date());UserVO userVO = new UserVO();BeanUtils.copyProperties(userVO, userDTO);System.out.println(JSON.toJSONString(userVO)); }4.BeanCopier
Cglib BeanCopier的原理與上面兩個Beanutils原理不太一樣,其主要使用CGlib字節(jié)碼技術(shù)動態(tài)生成一個代理類,代理類實現(xiàn)get和set方法。生成代理類過程存在一定開銷,但是一旦生成,我們可以緩存起來重復(fù)使用,所有Cglib性能相比以上兩種Beanutils性能比較好。
特點:
- 字段名不一致,屬性無法復(fù)制
- 類型不一致,屬性無法復(fù)制。如果類型為基本類型/基本類型的包裝類型,這兩者也無法被拷貝。但可自定義轉(zhuǎn)換器實現(xiàn)不同類型的拷貝。
- 淺拷貝
依賴:
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version> </dependency>方法:
/*** source 源Class* target 目Class* useConverter 是否使用轉(zhuǎn)換器* from 源對象* to 目標(biāo)對象* converter 轉(zhuǎn)換器*/ BeanCopier beanCopier = BeanCopier.create(Class source, Class target, boolean useConverter); beanCopier.copy(Object from, Object to, Converter converter);實現(xiàn):
public static void main(String[] args) {UserDTO userDTO = new UserDTO();userDTO.setId(1);userDTO.setUserName("哈哈");userDTO.setCreateTime(new Date());UserVO userVO = new UserVO();BeanCopier beanCopier = BeanCopier.create(UserDTO.class, UserVO.class, false);beanCopier.copy(userDTO, userVO, null);System.out.println(JSON.toJSONString(userVO)); }5.Orika
Orika也是一個跟Dozer類似的重量級屬性復(fù)制工具類,也提供諸如Dozer類似的功能。但是Orika無需使用繁瑣 XML配置,它自身提供一套非常簡潔的 API 用法,非常容易上手。
Orika底層基于Javassist生成字段屬性的映射的字節(jié)碼,然后直接動態(tài)加載執(zhí)行字節(jié)碼文件,相比于Dozer的這種使用反射原來的工具類,速度上會快很多。Orika的整個流程其實是需要使用到Java的反射的,只是在真正拷貝的屬性的時候沒有使用反射。
Orika的執(zhí)行流程:
Orikade的使用需要創(chuàng)建兩個對象MapperFactory與MapperFacade,其中MapperFactory 可以用于字段映射,配置轉(zhuǎn)換器等,而MapperFacade 的作用就與Beanutils一樣,用于負(fù)責(zé)對象的之間的映射。
特點:
- 默認(rèn)支持類型不一致(基本類型/包裝類型)轉(zhuǎn)換
- 指定不同字段名映射關(guān)系,屬性可以被成功復(fù)制
- 深拷貝
依賴:
<dependency><groupId>ma.glasnost.orika</groupId><artifactId>orika-core</artifactId><version>1.5.4</version> </dependency>方法:
/*** sourceObject 源對象* destinationClass 目標(biāo)Class* targetObject 目標(biāo)對象*/ MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build(); MapperFacade mapper = mapperFactory.getMapperFacade(); D targetObject = mapper.map(S sourceObject, Class<D> destinationClass);實現(xiàn):
public static void main(String[] args) {UserDTO userDTO = new UserDTO();userDTO.setId(1);userDTO.setUserName("哈哈");userDTO.setCreateTime(new Date());MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();MapperFacade mapper = mapperFactory.getMapperFacade();UserVO userVO = mapper.map(userDTO, UserVO.class);System.out.println(JSON.toJSONString(userVO)); }6.Dozer
Dozer相對BeanUtils這類工具類來說,擁有許多高級功能,所以相對來說這是一個重量級工具類。其底層本質(zhì)上還是使用了反射完成屬性的復(fù)制(屬性映射,遞歸的方式復(fù)制對象),所以執(zhí)行速度并不是那么理想。
Dozer需要我們新建一個DozerBeanMapper,這個類作用等同與BeanUtils,負(fù)責(zé)對象之間的映射,屬性復(fù)制。
生成DozerBeanMapper實例需要加載配置文件,隨意生成代價比較高。因此在我們應(yīng)用程序中,應(yīng)該盡量使用單例模式,重復(fù)使用DozerBeanMapper。
另外,強大的配置功能,我們可以通過XML、API或注解的方式配置源對象和目標(biāo)對象屬性映射關(guān)系和類型轉(zhuǎn)換。
特點:
- 類型不一致的字段,屬性被復(fù)制
- 通過配置字段名的映射關(guān)系,不一樣字段的屬性也被復(fù)制
- 深拷貝
依賴:
<dependency><groupId>net.sf.dozer</groupId><artifactId>dozer</artifactId><version>5.4.0</version> </dependency>方法:
/*** source 源對象* destinationClass 目標(biāo)Class* target 目標(biāo)對象*/ DozerBeanMapper mapper = new DozerBeanMapper(); T target = mapper.map(Object source, Class<T> destinationClass);實現(xiàn):
public static void main(String[] args) {UserDTO userDTO = new UserDTO();userDTO.setId(1);userDTO.setUserName("哈哈");userDTO.setCreateTime(new Date());DozerBeanMapper mapper = new DozerBeanMapper();UserVO userVO = mapper.map(userDTO, UserVO.class);System.out.println(JSON.toJSONString(userVO)); }7.MapStruct
MapStruct運行速度與硬編碼差不多,這是因為他在編譯期間就生成了Java Bean屬性復(fù)制的代碼(屬性對應(yīng)的get、set),運行期間就無需使用反射或者字節(jié)碼技術(shù),所以確保了高性能。
與硬編碼方式相比,不管使用反射,還是使用字節(jié)碼技術(shù),這些都需要在代碼運行期間動態(tài)執(zhí)行所以它們的執(zhí)行速度都會比硬編碼慢很多。
特點:
- 名不一致,默認(rèn)不支持復(fù)制
- 類型不一致,默認(rèn)不支持復(fù)制(但支持基本類型與包裝類型、基本類型的包裝類型與String的自動轉(zhuǎn)換)
- 可通注解配置實現(xiàn)名稱不一致、類型不一致的復(fù)制
- 是深拷貝
依賴:
<dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>1.3.1.Final</version> </dependency>插件:
由于MapStruct需要在編譯器期間生成代碼,所以我們需要maven-compiler-plugin插件中配置。
方法:
@Mapper public interface UserMapper {UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);@Mappings({@Mapping(source = "id", target = "id"),@Mapping(source = "createTime", target = "createTime")})UserVO dtoToVo(UserDTO userDTO); }編譯:
MapStruct沒有想象中的神奇,其實就是在編譯期生成了接口的實現(xiàn)類,里面的轉(zhuǎn)換方法實現(xiàn)了轉(zhuǎn)換功能。相當(dāng)于幫我們手寫get/set設(shè)值,所以它的性能會很好。
實現(xiàn):
public static void main(String[] args) {UserDTO userDTO = new UserDTO();userDTO.setId(1);userDTO.setUserName("哈哈");userDTO.setCreateTime(new Date());UserVO userVO = UserMapper.INSTANCE.dtoToVo(userDTO);System.out.println(JSON.toJSONString(userVO)); }可能出現(xiàn)的問題:
- 如果我們對象使用 Lombok 的話,使用 @Mapping指定不同字段名,編譯期間可能會拋出如下的錯誤
原因主要是因為Lombok也需要編譯期間自動生成代碼,這就可能導(dǎo)致兩者沖突,當(dāng)MapStruct生成代碼時,還不存在Lombok生成的代碼。解決辦法可以在 maven-compiler-plugin插件配置中加入Lombok。
8.Bean Mapping
基于反射的屬性拷貝。0.0.2版本引入了@BeanMapping,通過@BeanMapping注解可實現(xiàn)靈活的復(fù)制方式。
注解定義在 bean-mapping-api 模塊中,bean-mapping-core 會默認(rèn)引入此模塊。
特點:
- 名不一致,不支持復(fù)制
- 類型不一致,不支持復(fù)制(但支持基本類型轉(zhuǎn)為包裝類型,反過來不支持)
- 通過@BeanMapping注解來靈活控制復(fù)制,支持名稱不一致的復(fù)制、類型不一致的復(fù)制和控制是否復(fù)制。
- 淺拷貝
依賴:
<dependency><groupId>com.github.houbb</groupId><artifactId>bean-mapping-core</artifactId><version>0.2.5</version> </dependency>注解:
@Inherited @Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface BeanMapping {/*** 字段別名* 如果不填,則默認(rèn)使用字段的名稱* 會將source的屬性值賦值給target和當(dāng)前name屬性一致的屬性* @return 字段別名*/String name() default "";/*** 生效條件(默認(rèn)為生效)* 1.當(dāng)放在source字段上時,表示是否將值賦給target字段* 2.當(dāng)放在target字段上時,表示是否接受賦值。* 3.source+target只有同時生效時,才會發(fā)生賦值。* @return 具體的生效實現(xiàn)*/Class<? extends ICondition> condition() default ICondition.class;/*** 類型轉(zhuǎn)換(默認(rèn)不進行轉(zhuǎn)換)* 當(dāng)source的值轉(zhuǎn)換后可以設(shè)置給target,才會將source轉(zhuǎn)換后的值賦值給target對應(yīng)屬性,其他情況不會對值產(chǎn)生影響。* @return 具體的轉(zhuǎn)換實現(xiàn)*/Class<? extends IConvert> convert() default IConvert.class;}方法:
/*** source 源對象* target 目標(biāo)對象*/ BeanUtil.copyProperties(Object source, Object target)實現(xiàn):
public static void main(String[] args) {UserDTO userDTO = new UserDTO();userDTO.setId(1);userDTO.setUserName("哈哈");userDTO.setCreateTime(new Date());UserVO userVO = new UserVO();BeanUtil.copyProperties(userDTO, userVO);System.out.println(JSON.toJSONString(userVO)); }9.Bean Mapping ASM
推薦:★★★☆☆
性能:★★★★☆
手段:基于ASM字節(jié)碼框架實現(xiàn)
點評:與普通的Bean Mapping 相比,性能有所提升,可以使用。
Bean Mapping基于ASM的字節(jié)碼增強技術(shù)的復(fù)制方式要比Bean Mapping普通的方式新能要提升不少,但有個缺點就是暫不支持@BeanMapping注解等更加豐富的功能。
特點:
- 名不一致,不支持復(fù)制
- 類型不一致,不支持復(fù)制(但支持基本類型轉(zhuǎn)為包裝類型,反過來不支持)
- 效率比傳統(tǒng)的Bean Mapping要好些,但暫不支持@BeanMapping注解的靈活復(fù)制
- 淺拷貝
依賴:
<dependency><groupId>com.github.houbb</groupId><artifactId>bean-mapping-asm</artifactId><version>0.2.5</version> </dependency>方法:
/*** source 源對象* target 目標(biāo)對象*/ AsmBeanUtil.copyProperties(Object source, Object target)實現(xiàn):
public static void main(String[] args) {UserDTO userDTO = new UserDTO();userDTO.setId(1);userDTO.setUserName("哈哈");userDTO.setCreateTime(new Date());UserVO userVO = new UserVO();AsmBeanUtil.copyProperties(userDTO, userVO);System.out.println(JSON.toJSONString(userVO)); }10.ModelMapper
ModelMapper是利用反射的原理實現(xiàn)的。轉(zhuǎn)換對象數(shù)量較少時性能不錯,如果同時大批量轉(zhuǎn)換對象,性能有所下降。
依賴:
<dependency><groupId>org.modelmapper</groupId><artifactId>modelmapper</artifactId><version>2.3.0</version> </dependency>實現(xiàn):
簡單使用
ModelMapper的具體使用可參考文章:實體映射類庫(modelmapper和MapStruct)
11.JMapper
JMapper通過映射器方式實現(xiàn)。
依賴:
<dependency><groupId>com.googlecode.jmapper-framework</groupId><artifactId>jmapper-core</artifactId><version>1.6.0</version> </dependency>實現(xiàn):
public static void main(String[] args) {UserDTO userDTO = new UserDTO();userDTO.setId(1);userDTO.setUserName("哈哈");userDTO.setCreateTime(new Date());JMapper<UserVO, UserDTO> jMapper = new JMapper<>(UserVO.class, UserDTO.class, new JMapperAPI().add(JMapperAPI.mappedClass(UserVO.class).add(JMapperAPI.attribute("id").value("id")).add(JMapperAPI.attribute("userName").value("userName")).add(JMapperAPI.attribute("createTime").value("createTime"))));UserVO userVO = jMapper.getDestination(userDTO);System.out.println(JSON.toJSONString(userVO)); }12.Json2Json
這種通過JSON序列化和反序列化的方式,把源對象轉(zhuǎn)為JSON串,再把JSON串轉(zhuǎn)為目標(biāo)對象,雖然也能達到復(fù)制的目的,但不推薦使用。
實現(xiàn):
public static void main(String[] args) {UserDTO userDTO = new UserDTO();userDTO.setId(1);userDTO.setUserName("哈哈");userDTO.setCreateTime(new Date());UserVO userVO = JSON.parseObject(JSON.toJSONString(userDTO), UserVO.class);System.out.println(JSON.toJSONString(userVO)); }復(fù)制方案選擇
參考文章:對比 12 種 Bean 自動映射工具
總結(jié)
- 上一篇: displayTag获得行号
- 下一篇: 有考c语言的软件工程专硕吗,2020年南