Java常见拷贝
System.arraycopy()
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,int length);
- src:源数组
- srcPos:源数组要复制起始的位置
- dest:目的数组
- destPos::目的数组放置的起始位置
- length:源数组复制的长度
native方法,所以效率比较高 。
public class CopyObjectDemo {
public static void main(String[] args){
char[] src = "123456".toCharArray();
char[] dest = "abcdefg".toCharArray();
System.arraycopy(src, 1, dest, 2, 5);
System.out.println("src=" + new String(src));
System.out.println("dest=" + new String(dest));
}
}
输出:
src=123456
dest=ab23456
注:src和dest必须是同类型或者可以进行转换类型的数组
Arrays.copyOf()
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)?(T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
return copy;
}
//底层调用的是System.arraycopy
- original:要复制的数组
- newLength:新的数组长度
- newType:要返回的副本类型
Arrays的copyOf()方法传回的数组是新的数组对象,改变传回数组中的元素值,不会影响原来的数组。
copyOf()的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值。
调用:
public class CopyObjectDemo {
public static void main(String[] args) {
int[] arr1 = {1, 2, 3, 4, 5};
int[] arr2 = Arrays.copyOf(arr1, 5);
int[] arr3 = Arrays.copyOf(arr1, 10);
for(int i = 0; i < arr2.length; i++)
System.out.print(arr2[i] + " ");
arr3[0] = 6;
System.out.println();
for(int i = 0; i < arr3.length; i++)
System.out.print(arr3[i] + " ");
}
}
输出:
1 2 3 4 5
6 2 3 4 5 0 0 0 0 0
Arrays.copyOfRange()
源码:
public static int[] copyOfRange(int[] original, int from, int to)
- from 起始坐标
- to 终止坐标
从下标from开始复制,复制到上标to,生成一个新的数组。注:包括下标from,不包括上标to
public class CopyObjectDemo {
public static void main(String[] args) {
int[] arr1 = {1, 2, 3, 4, 5};
int[] arr2 = Arrays.copyOfRange(arr1, 1, 3);
for(int i = 0; i < arr2.length; i++)
System.out.print(arr2[i] + " ");
}
}
输出:
2 3
框架常见拷贝
实际项目开发过程中,我们经常需要将不同的两个对象实例进行属性复制,比如DTO数据传输对象和数据对象DO,我们需要将DO对象进行属性复制到DTO,但是对象格式又不一样,所以我们需要编写映射代码将对象中的属性值从一种类型转换成另一种类型。
这种转换最原始的方式就是手动编写大量的get/set,使得开发繁琐,为了解决这一痛点,就诞生了一些方便的类库,Apache的BeanUtils、Spring的BeanUtils、Apache的PropertyUtils、Cglib的BeanCopier、orika等等。
Apache的BeanUtils
需要导入 org.apache.commons.beanutils.BeanUtils
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
BeanUtilsBean.getInstance().copyProperties(dest, orig);
}
注:copyProperties第一个参数是目的对象,第二个参数是源地址。(特别反人类的设计…( ╯□╰ ))
package 创建对象;
import org.apache.commons.beanutils.BeanUtils;
import java.lang.reflect.InvocationTargetException;
public class TestApacheBeanUtils {
public static void main(String[] args) {
// org.apache.commons.beanutils.BeanUtils
Score scoreSrc = new Score("math", "95", new Level("5","30"));
Score scoreDest = new Score();
try {
BeanUtils.copyProperties(scoreDest, scoreSrc);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
System.out.println("scoreSrc: "+ scoreSrc);
System.out.println("scoreDest: "+ scoreDest);
change(scoreDest);
System.out.println("scoreSrc: "+ scoreSrc);
System.out.println("scoreDest: "+ scoreDest);
}
public static void change(Score score){
score.grade = "90";
score.level.classLevel = "10";
}
}
输出:
scoreSrc: Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}}
scoreDest: Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}}
scoreSrc: Score{course='math', grade='95', level=Level{classLevel='10', gradeLevel='30'}}
scoreDest: Score{course='math', grade='90', level=Level{classLevel='10', gradeLevel='30'}}
结果看出,Apache的BeanUtils.copyProperties是浅拷贝。
需要注意,谨慎使用这个copyproperties这个功能,相同的属性都会被替换,不管是否有值。
由于 Apache下的BeanUtils对象拷贝性能太差,不建议使用,而且在阿里巴巴Java开发规约插件上也明确指出不使用。
Map集合封装JavaBean
public static void populate(Object bean, Map<String, ? extends Object> properties) throws IllegalAccessException, InvocationTargetException {
BeanUtilsBean.getInstance().populate(bean, properties);
}
BeanUtils.populate(Object bean, Map<String, ? extends Object> properties),将Map集合封装JavaBean。
public class TestApacheBeanUtils {
public static void main(String[] args) {
Score scoreDest = new Score();
Map map = new HashMap<String, Object>();
map.put("course", "English");
map.put("grade", "90");
map.put("level", new Level("15", "50"));
try {
BeanUtils.populate(scoreDest, map);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
System.out.println("scoreDest: "+ scoreDest);
}
}
输出:
scoreDest: Score{course='English', grade='90', level=Level{classLevel='15', gradeLevel='50'}}
注:如果map的key和目的类的属性名称不一样,会转换失败,抛出异常 “java.lang.NoClassDefFoundError”。
Spring的BeanUtils
需要导入 org.springframework.beans.BeanUtils
public static void copyProperties(Object source, Object target) throws BeansException {
copyProperties(source, target, (Class)null, (String[])null);
}
注:copyProperties第一个参数是源地址,第二个参数是目的对象。
- target和source的属性名类型和名称必须相同
- 名称大小写敏感
- 如果名称大小写不一致,或者类型不一致,则跳过,不做属性复制
相同类型拷贝
package 创建对象;
import org.springframework.beans.BeanUtils;
public class TestApacheBeanUtils {
public static void main(String[] args) {
// org.springframework.beans.BeanUtils
Score scoreSrc = new Score("math", "95", new Level("5","30"));
Score scoreDest = new Score();
BeanUtils.copyProperties(scoreSrc, scoreDest);
System.out.println("scoreSrc: "+ scoreSrc);
System.out.println("scoreDest: "+ scoreDest);
change(scoreDest);
System.out.println("scoreSrc: "+ scoreSrc);
System.out.println("scoreDest: "+ scoreDest);
}
public static void change(Score score){
score.grade = "90";
score.level.classLevel = "10";
}
}
输出:
scoreSrc: Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}}
scoreDest: Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}}
scoreSrc: Score{course='math', grade='95', level=Level{classLevel='10', gradeLevel='30'}}
scoreDest: Score{course='math', grade='90', level=Level{classLevel='10', gradeLevel='30'}}
结果看出,Spring的BeanUtils.copyProperties是浅拷贝。
使用Spring的BeanUtils.copyProperties,类必须含有 getter/setter方法。它的实现方式非常简单,就是对两个对象中相同名字的属性进行简单的get/set,仅检查属性的可访问性。
不同类型拷贝
新增类Score2,成员变量名称为grade2。
public class Score2 {
public String course;
public String grade2;
public Level level;
public Score2() { }
public Score2(String course, String grade, Level level) {
this.course = course;
this.grade2 = grade;
this.level = level;
}
public String getCourse() {
return course;
}
public void setCourse(String course) {
this.course = course;
}
public String getGrade2() {
return grade2;
}
public void setGrade2(String grade2) {
this.grade2 = grade2;
}
public Level getLevel() {
return level;
}
public void setLevel(Level level) {
this.level = level;
}
@Override
public String toString() {
return "Score{" +"course='" + course + '\'' +", grade='" + grade2 + '\'' +", level=" + level +'}';
}
}
调用:
public class TestApacheBeanUtils {
public static void main(String[] args) {
// org.springframework.beans.BeanUtils
Score scoreSrc = new Score("math", "95", new Level("5","30"));
Score2 scoreDest = new Score2();
BeanUtils.copyProperties(scoreSrc, scoreDest);
System.out.println("scoreSrc: "+ scoreSrc);
System.out.println("scoreDest: "+ scoreDest);
change(scoreDest);
System.out.println("scoreSrc: "+ scoreSrc);
System.out.println("scoreDest: "+ scoreDest);
}
public static void change(Score2 score){
score.grade2 = "90";
if (null != score.level) {
score.level.classLevel = "10";
}
}
}
输出:
scoreSrc: Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}}
scoreDest: Score{course='math', grade='null', level=Level{classLevel='5', gradeLevel='30'}}
scoreSrc: Score{course='math', grade='95', level=Level{classLevel='10', gradeLevel='30'}}
scoreDest: Score{course='math', grade='90', level=Level{classLevel='10', gradeLevel='30'}}
可以看到,成员变量赋值是基于目标对象的成员列表,并且会跳过ignore的以及在源对象中不存在,所以这个方法是安全的,不会因为两个对象之间的结构差异导致错误,但是必须保证同名的两个成员变量类型相同。
public static void copyProperties(Object source, Object target, String… ignoreProperties) throws BeansException {
copyProperties(source, target, (Class)null, ignoreProperties);
}
可以指定忽略的属性名称数组。用于指定不想被覆盖的属性。
CGLIB BeanCopier
基于CGLIB代理,CGLIB(Code Generation Library)是高效的代码生成包,底层依靠ASM(开源的Java字节码编辑类库)操作字节码实现。
BeanCopier.create会针对源类和目标类生成代理类(此处有反射),而且有一定时间上的消耗,但BeanCopier.copy并未使用反射。它正是巧妙的使用这点,将反射部分(性能差)与生成后硬编码分离!这也是CGLIB BeanCopier高效的原因!
cglib 的BeanCopier高性能解密
https://blog.csdn.net/alex_xfboy/article/details/88966201?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-88966201-blog-78363191.pc_relevant_paycolumn_v3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-88966201-blog-78363191.pc_relevant_paycolumn_v3&utm_relevant_index=2
1、指定BeanCopier
public static BeanCopier create(Class source, Class target, boolean useConverter) {}
2、调用copy方法
public abstract void copy(Object var1, Object var2, Converter var3);
实际使用:
package 创建对象;
import org.springframework.cglib.beans.BeanCopier;
public class TestApacheBeanUtils {
public static void main(String[] args) {
Score scoreSrc = new Score("math", "95", new Level("5","30"));
Score2 scoreDest = new Score2();
final BeanCopier copier = BeanCopier.create(Score.class, Score2.class, false);
copier.copy(scoreSrc, scoreDest, null);
System.out.println("scoreSrc: "+ scoreSrc);
System.out.println("scoreDest: "+ scoreDest);
change(scoreDest);
System.out.println("scoreSrc: "+ scoreSrc);
System.out.println("scoreDest: "+ scoreDest);
}
public static void change(Score2 score){
score.grade2 = "90";
if (null != score.level) {
score.level.classLevel = "10";
}
}
}
输出:
scoreSrc: Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}}
scoreDest: Score{course='math', grade='null', level=Level{classLevel='5', gradeLevel='30'}}
scoreSrc: Score{course='math', grade='95', level=Level{classLevel='10', gradeLevel='30'}}
scoreDest: Score{course='math', grade='90', level=Level{classLevel='10', gradeLevel='30'}}
结果和Spring的BeanUtils.copyProperties一样,会拷贝名称相同且类型相同的属性,名称相同而类型不同的属性不会被拷贝、名称不相同当然也不会被拷贝。而且CGLIB BeanCopier 是浅拷贝。
注意:即使源类型是 原始类型(int, short和char等),目标类型是其 包装类型(Integer, Short和Character等),或反之:都 不会被拷贝。
- BeanCopier只拷贝名称和类型都相同的属性。
- 当目标类的setter数目比getter少时,创建BeanCopier会失败而导致拷贝不成功。
自定义Converter
自定义Converter 只能对同名不同类型的属性进行转换,不同名的属性没法操作。
详见https://www.iteye.com/blog/czj4451-2044101
缓存BeanCopier提升性能
BeanCopier拷贝速度快,性能瓶颈出现在创建BeanCopier实例的过程中。
所以,把创建过的BeanCopier实例放到缓存中,下次可以直接获取,提升性能。
public class CachedBeanCopier {
static final Map<String, BeanCopier> BEAN_COPIERS = new HashMap<String, BeanCopier>();
public static void copy(Object srcObj, Object destObj) {
String key = genKey(srcObj.getClass(), destObj.getClass());
BeanCopier copier = null;
if (!BEAN_COPIERS.containsKey(key)) {
copier = BeanCopier.create(srcObj.getClass(), destObj.getClass(), false);
BEAN_COPIERS.put(key, copier);
} else {
copier = BEAN_COPIERS.get(key);
}
copier.copy(srcObj, destObj, null);
}
private static String genKey(Class<?> srcClazz, Class<?> destClazz) {
return srcClazz.getName() + destClazz.getName();
}
}
MapStucts
可以字段映射,比如Score的属性grade 映射拷贝给 Score2的属性grade2。
https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=2650139073&idx=1&sn=8834525faf92fc03b8b5140370791cbb&chksm=f36bf4e0c41c7df62f5eb504c8cdecaecb5e33a8482b566376d020cb876094fa62c9c382a981&scene=21#wechat_redirect
orika
Orika是近期在github活跃的项目,底层采用了javassist类库生成Bean映射的字节码,之后直接加载执行生成的字节码文件,因此在速度上比使用反射进行赋值会快很多,下面详细介绍Orika的使用方法。
依赖:
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.5.2</version><!-- or latest version -->
</dependency>
orika可以进行字段映射,详见https://www.cnblogs.com/songhaibin/p/13382799.html
public class OrikaTest {
public static void main(String[] args) {
Score scoreSrc = new Score("math", "95", new Level("5","30"));
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
mapperFactory.classMap(Score.class, Score2.class)
.byDefault()
.register();
MapperFacade mapper = mapperFactory.getMapperFacade();
Score2 scoreDest = mapper.map(scoreSrc, Score2.class);
System.out.println("scoreSrc: "+ scoreSrc);
System.out.println("scoreDest: "+ scoreDest);
change(scoreDest);
System.out.println("scoreSrc: "+ scoreSrc);
System.out.println("scoreDest: "+ scoreDest);
}
public static void change(Score2 score){
score.grade = "90";
if (null != score.level) {
score.level.classLevel = "10";
}
}
}
输出:
scoreSrc: Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}}
scoreDest: Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}}
scoreSrc: Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}}
scoreDest: Score{course='math', grade='90', level=Level{classLevel='10', gradeLevel='30'}}
结果看出,orika实现对象拷贝是深拷贝
深拷贝
上述Apache的BeanUtils、Spring的BeanUtils、CGLIB BeanCopier均为浅拷贝,如何实现深拷贝呢?
之前文章《Java对象拷贝》介绍实现Serializable接口、ObjectOutputStream 序列化实现深拷贝。今天介绍新的方法:
- 使用各种JSON工具,把对象序列化成JSON字符串,然后再从字符串中反序列化成对象。如使用fastjson实现。
- Apache Commons Lang中提供的SerializationUtils工具实现。
fastjson实现
Score newScore = JSON.parseObject(JSON.toJSONString(score), Score.class);
SerializationUtils工具
第一步:Score 、Level 实现Serializable接口,否则是无法进行序列化的。
public class Score implements Serializable
public class Level implements Serializable
第二步:拷贝
Score newScore = (Score) SerializationUtils.clone(score);
小结
- 不同类之间拷贝方法有Apache的BeanUtils、Spring的BeanUtils、CGLIB BeanCopier均为浅拷贝,浅拷贝的结果就是两个对象中的引用对象都是同一个地址,只要发生改变,都会有影响。
- orika实现对象拷贝是深拷贝。
- 当属性名和属性类型完全相同时使用CGLIB BeanCopier是最好的选择,当存在属性名称不同或者属性名称相同但属性类型不同的情况时,使用Orika是一种不错的选择,还可以使用MapStucts。
- 实现深拷贝,有很多种办法,其中比较常用的就是实现Cloneable接口重写clone方法,还有使用序列化+反序列化创建新对象。
参考:
https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=2650164239&idx=1&sn=51e80874aa7679f7d0c8e64ff3f43a8a&chksm=f368512ec41fd8384ac55cb85c104883d0ccd8b5ddff48b7244dc77c356f4e57769a24951df1&mpshare=1&scene=23&srcid=0727fq2ozSJIbzx6M4CUhKcY&sharer_sharetime=1627385888625&sharer_shareid=819e1ae410b8cb57b85554513e37a8ca#rd