有关java对象拷贝操作的总结

本文详细介绍了Java中对象拷贝的各种方式,包括手动get&set、Cglib的BeanCopier、Apache的BeanUtils、PropertyUtils以及Spring的BeanUtils。对比了它们之间的性能和区别,指出Cglib BeanCopier在性能上表现优秀。此外,文章讨论了浅拷贝和深拷贝的概念,解释了默认赋值操作为浅拷贝,并提供了实现深拷贝的两种方法:实现Cloneable接口和使用序列化。通过代码示例展示了这两种深拷贝的实现方式。

一、引言

常用的拷贝方式:

  1. 手动get&set实现。
  2. 通过Cglib的BeanCopier实现。
  3. 通过Apache的BeanUtils实现。
  4. 通过Apache的PropertyUtils实现。
  5. 通过Spring的BeanUtils实现。

(此模块先对常用拷贝方式进行总结,后文会详细介绍深拷贝&浅拷贝的区别以及实现方式。)

二.常用拷贝方式之间的区别

1. 手动get&set实现:

简单,粗暴,高性能

2. 通过CglibBeanCopier实现(使用动态代理,效率高):

不使用类型转换器:

BeanCopier beanCopier = BeanCopier.create(SourceClass.class, TargetClass.class, false); 前赋后

beanCopier.copy(sourceClass, targetClass, null);

不支持不同类型字段的转换,但可通过自己实现的converter转换器实现类型之间的转换。

使用类型转换器:

BeanCopier beanCopier = BeanCopier.create(SourceClass.class, TargetClass.class, true); 前赋后

beanCopier.copy(sourceClass, targetClass, converter); //converter为自己实现的转换器。

例如:

public class Long2StringConvert implements Converter {

    @Override
    public Object convert(Object sourceValue, Class targetClass, Object methodName) {
        if( null != sourceValue && sourceValue.getClass().equals(Long.class) && targetClass.equals(String.class)) {
            return String.valueOf(sourceValue);
        }
        return sourceValue;
    }
}

生成用于复制两个类的BeanCoper类:

public final static BeanCopier dir2DirVOCopier = BeanCopier.create(Dir.class,DirVO.class,true);

执行拷贝:

BeanCopyProvider.dir2DirVOCopier.copy(dir, vo, new Long2StringConvert());

参考文档:

CGLIB中BeanCopier源码实现 - 简书 // CGLIB中BeanCopier源码实现

对象拷贝类PropertyUtils,BeanUtils,BeanCopier的技术沉淀(1)------功能简介_express_wind的专栏-CSDN博客

对象拷贝类PropertyUtils,BeanUtils,BeanCopier的技术沉淀(2)------缺陷预防_express_wind的专栏-CSDN博客

对象拷贝类PropertyUtils,BeanUtils,BeanCopier的技术沉淀(3)------优化方案_express_wind的专栏-CSDN博客_beancopier和beanutils

3. 通过Apache的BeanUtils实现(反射机制):

使用:BeanUtils.copyProperties(targetClass, sourceClass);  后赋前

提供自动转换功能,即发现两个JavaBean的同名属性为不同类型时,在支持的数据类型范围内进行转换

BeanUtils支持的转换类型如下:

* java.lang.BigDecimal

* java.lang.BigInteger

* boolean and java.lang.Boolean

* byte and java.lang.Byte

* char and java.lang.Character

* java.lang.Class

* double and java.lang.Double

* float and java.lang.Float

* int and java.lang.Integer

* long and java.lang.Long

* short and java.lang.Short

* java.lang.String

* java.sql.Date

* java.sql.Time

* java.sql.Timestamp

这里要注意一点,java.util.Date是不被支持的,而它的子类java.sql.Date是被支持的。因此如果对象包含时间类型的属性,且注意被转换的时候,一定要使用java.sql.Date类型。否则在转换时会提示argument mistype异常。

可使用转换器beanutils.Converter,增强apache的beanUtils的拷贝属性,注册一些新的类型转换。

例:

ConvertUtils.register(new DateConvert(), java.util.Date.class);

4. 通过Apache的PropertyUtils实现(反射机制):

使用:PropertyUtils.copyProperties(targetClass, sourceClass);  后赋前

不提供自动转换功能。当类型不一致时会抛出异常。

5. 通过Spring的BeanUtils实现(反射机制):

使用:BeanUtils.copyProperties(sourceClass,targetClass); 前赋后

BeanUtils.copyProperties(sourceClass,targetClass,ignoreProperties);

不支持不同类型之间的转换。

三、性能测试

测试代码:

​
Data.java:

@ToString
@lombok.Data
public class Data {

    private String aa;
    private String ab;
    private String ac;
    private String ad;
    private String ae;
    private String af;
    private String ag;
    private Map map;


    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

PerformanceTesting.java:

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.springframework.cglib.beans.BeanCopier;
import java.util.HashMap;
import java.util.Map;

public class PerformanceTesting {

    public static void main(String[] args) {

        try {
            //性能测试
            Data sourceClass = new Data();
            sourceClass.setAa("a");
            sourceClass.setAb("b");
            sourceClass.setAc("c");
            sourceClass.setAd("d");
            sourceClass.setAe("e");
            sourceClass.setAf("f");
            sourceClass.setAg("g");
            sourceClass.setAd("d");
            sourceClass.setAd("d");

            Map<String, String> map = new HashMap<String, String>();
            map.put("test1", "test1");
            map.put("test2", "test2");
            map.put("test3", "test3");
            sourceClass.setMap(map);



            BeanCopier bc = BeanCopier.create(Data.class, Data.class, false);

            int number = 10000;
            System.out.println("\n----  number = " + number + "  ----\n");


            //beanCoper
            long time0 = System.currentTimeMillis();
            for(int i = 0; i < number; i++) {
                beanCopier(sourceClass, bc);
            }
            System.out.println("Cglib BeanCopier :: " + (System.currentTimeMillis()-time0));


            //get & set
            long time1 = System.currentTimeMillis();
            for(int i = 0; i < number; i++) {
                getAndSet(sourceClass);
            }
            System.out.println("getAndSet :: " + (System.currentTimeMillis()-time1));


            //apache Properties
            long time2 = System.currentTimeMillis();
            for(int i = 0; i < number; i++) {
                propertyUtils(sourceClass);
            }
            System.out.println("apache PropertyUtils :: " + (System.currentTimeMillis()-time2));


            //apache BeanUtils
            long time3 = System.currentTimeMillis();
            for(int i = 0; i < number; i++) {
                apacheBeanUtils(sourceClass);
            }
            System.out.println("apache BeanUtils :: " + (System.currentTimeMillis()-time3));


            //Spring BeanUtils
            long time4 = System.currentTimeMillis();
            for(int i = 0; i < number; i++) {
                springBeanUtils(sourceClass);
            }
            System.out.println("spring BeanUtils :: " + (System.currentTimeMillis()-time4));
        }catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static void beanCopier(Data sourceClass, BeanCopier bc) {
        Data targetClass = new Data();
        bc.copy(sourceClass, targetClass, null);
    }

    public static void getAndSet(Data sourceClass) {
        Data targetClass = new Data();
        targetClass.setAa(sourceClass.getAa());
        targetClass.setAb(sourceClass.getAb());
    }

    public static void springBeanUtils(Data sourceClass) {
        Data targetClass = new Data();
        org.springframework.beans.BeanUtils.copyProperties(sourceClass, targetClass); //前赋后
    }

    public static void apacheBeanUtils(Data sourceClass) throws Exception {
        Data targetClass = new Data();
        BeanUtils.copyProperties(targetClass, sourceClass); //后赋前
    }

    public static void propertyUtils(Data sourceClass) throws Exception {
        Data targetClass = new Data();
        PropertyUtils.copyProperties(targetClass, sourceClass); //后赋前
    }
}

​

测试结果:

100次

1000次

1万次

1亿次

Cglib BeanCopier

0

0

3

15

getAndSet

0

0

0

7

apache PropertyUtils

85

106

216

386233

apache BeanUtils

11

40

179

668751

spring BeanUtils

80

63

86

33286

结论:

  • 综上,性能测试结果:getAndSet > Cglib BeanCopier > apache BeanUtils  >  spring BeanUtils > apache PropertyUtils
  • bean copy场景较少或者对性能要求较高的部分避免使用任何bean copy框架
  • 如果要使用bean copy框架,优先使用cglib。同时需要注意:cglib使用BeanCopier.create()也是非常耗时,避免多次调用,尽可能做成全局初始化一次

四.遗留问题:

目标类set方法返回值不为void时,BeanCoper无法拷贝。

后在BeanCopier#generateClass()方法中找到答案:

public void generateClass(ClassVisitor v) {
    Type sourceType = Type.getType(this.source);
    Type targetType = Type.getType(this.target);
    ClassEmitter ce = new ClassEmitter(v);
    ce.begin_class(46, 1, this.getClassName(), BeanCopier.BEAN_COPIER, (Type[])null, "<generated>");
    EmitUtils.null_constructor(ce);
    CodeEmitter e = ce.begin_method(1, BeanCopier.COPY, (Type[])null);
    PropertyDescriptor[] getters = ReflectUtils.getBeanGetters(this.source);
    PropertyDescriptor[] setters = ReflectUtils.getBeanSetters(this.target);
    Map names = new HashMap();

    for(int i = 0; i < getters.length; ++i) {
        names.put(getters[i].getName(), getters[i]);
    }

    Local targetLocal = e.make_local();
    Local sourceLocal = e.make_local();
    if (this.useConverter) {
        e.load_arg(1);
        e.checkcast(targetType);
        e.store_local(targetLocal);
        e.load_arg(0);
        e.checkcast(sourceType);
        e.store_local(sourceLocal);
    } else {
        e.load_arg(1);
        e.checkcast(targetType);
        e.load_arg(0);
        e.checkcast(sourceType);
    }

    for(int i = 0; i < setters.length; ++i) {
        PropertyDescriptor setter = setters[i];
        PropertyDescriptor getter = (PropertyDescriptor)names.get(setter.getName());
        if (getter != null) {
            MethodInfo read = ReflectUtils.getMethodInfo(getter.getReadMethod());
            MethodInfo write = ReflectUtils.getMethodInfo(setter.getWriteMethod());
            if (this.useConverter) {
                Type setterType = write.getSignature().getArgumentTypes()[0]; //获取参数类型
                e.load_local(targetLocal);
                e.load_arg(2);
                e.load_local(sourceLocal);
                e.invoke(read);
                e.box(read.getSignature().getReturnType());
                EmitUtils.load_class(e, setterType);
                e.push(write.getSignature().getName());
                e.invoke_interface(BeanCopier.CONVERTER, BeanCopier.CONVERT);
                e.unbox_or_zero(setterType);
                e.invoke(write);
            } else if (compatible(getter, setter)) {
                e.dup2();
                e.invoke(read);
                e.invoke(write);
            }
        }
    }

    e.return_value();
    e.end_method();
    ce.end_class();
}
private static boolean compatible(PropertyDescriptor getter, PropertyDescriptor setter) {
    return setter.getPropertyType().isAssignableFrom(getter.getPropertyType());
}
public native boolean isAssignableFrom(Class<?> cls);  //校验返回值是否与方法名匹配

五.浅拷贝&深拷贝

一般使用 『 = 』号做赋值操作的时候,对于基本数据类型,实际上是拷贝的它的值,但是对于对象而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,他们实际上还是指向的同一个对象。

而浅拷贝和深拷贝就是在这个基础之上做的区分,如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行了引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝。

1、浅拷贝

对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。

 

2、深拷贝

对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

 

经验证,BeanCoper和Spring的BeanUtils在拷贝对象时使用的是浅拷贝。

测试代码如下:

TestData.java:

import com.bj58.beanCoper.Data;
import java.util.Map;
import lombok.ToString;

@ToString
@lombok.Data
public class TestData {

    private Data data;
    private Map map;
    private String string;

}

CopyTest.java:

import com.bj58.beanCoper.Data;
import org.springframework.beans.BeanUtils;
import org.springframework.cglib.beans.BeanCopier;
import java.util.HashMap;
import java.util.Map;

public class CopyTest {

    private final static BeanCopier beanCopier = BeanCopier.create(TestData.class, TestData.class, false);

    public static void main(String[] args) {

        TestData sourceClass = new TestData();

        final Map<String, String> map = new HashMap<String, String>();
        map.put("test1", "test1");
        map.put("test2", "test2");
        map.put("test3", "test3");

        sourceClass.setMap(map);
        sourceClass.setString("test");
        sourceClass.setData(new Data(){{
            setAa("11");
            setAb("11");
            setAc("11");
            setAd("11");
            setAe("11");
            setAf("11");
            setAg("11");
            setMap(map);
        }});



        //  beanCoperCopy
        TestData beanCoperTargetClass = beanCoperCopy(sourceClass);
        System.out.println(beanCoperTargetClass);
        System.out.println("直接使用beanCopier赋值后,sourceClass.getData() == beanCoperTargetClass.getData()值为 " + (sourceClass.getData() == beanCoperTargetClass.getData()));

        //  beanUtilsCopy
        TestData beanUtilsTargetClass = beanUtilsCopy(sourceClass);
        System.out.println(beanUtilsTargetClass);
        System.out.println("直接使用beanUtils赋值后,sourceClass.getData() == beanUtilsTargetClass.getData()值为 " + (sourceClass.getData() == beanUtilsTargetClass.getData()));


        System.out.println("\n ---------  修改源对象数据  ----------");
        System.out.println("修改前 sourceClass.getData().getAg()值为 " + sourceClass.getData().getAg());
        sourceClass.getData().setAg("21");
        
        System.out.println("修改后 sourceClass.getData().getAg()值为 " + sourceClass.getData().getAg());
        System.out.println("修改后 beanCoperTargetClass.getData().getAg()值为 " + beanCoperTargetClass.getData().getAg());
        System.out.println("修改后 beanUtilsTargetClass.getData().getAg()值为 " + beanUtilsTargetClass.getData().getAg());

        System.out.println("修改了sourceClass的data值后,sourceClass.getData() == beanCoperTargetClass.getData()值为 " + (sourceClass.getData() == beanCoperTargetClass.getData()));
        System.out.println("修改了sourceClass的data值后,sourceClass.getData() == beanUtilsTargetClass.getData()值为 " + (sourceClass.getData() == beanUtilsTargetClass.getData()));
    }

    private static TestData beanCoperCopy(TestData sourceClass) {
        TestData targetClass = new TestData();
        beanCopier.copy(sourceClass, targetClass, null);
        return targetClass;
    }

    private static TestData beanUtilsCopy(TestData sourceClass) {
        TestData targetClass = new TestData();
        BeanUtils.copyProperties(sourceClass, targetClass);
        return targetClass;
    }
}

运行结果:

 

对象深拷贝的实现

  1. 实现Cloneable接口,重写对象的clone()方法

需要注意的点:

1)代码侵入较大

2)默认的clone()方法是浅拷贝,深拷贝是需要自己来实现的对于基本类型可以直接赋值,而对于对象、容器、数组来讲,需要创建一个新的出来,然后重新赋值

3)Java Cloneable接口实际上是个空接口,没有任何方法,实际的clone()是object的方法,但是是一个protected的方法,因此需要重写这个方法,并且声明为public,不然的话protected方法在其它包中是无法访问的。

4)虽然Cloneable接口是个空接口,JVM会作检查,如果不实现Cloneable接口则会抛出java.lang.CloneNotSupportedException异常。

2.使用序列化

如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以通过将当前对象序列化,然后再反序列化的方式来实现对象的深克隆。

测试代码:

import com.bj58.beanCoper.Data;
import java.io.*;
import java.util.HashMap;
import java.util.Map;

public class DeepCopy {
    public static void main(String[] args) throws IOException {
        ObjectOutputStream oos = null;
        ObjectInputStream ois = null;

        try {
            // 创建原始的可序列化对象
            TestData sourceClass = new TestData();

            final Map<String, String> map = new HashMap<String, String>();
            map.put("test1", "test11");
            map.put("test2", "test22");
            map.put("test3", "test33");

            sourceClass.setMap(map);
            sourceClass.setString("test");
            sourceClass.setData(new Data(){{
                setAa("11");
                setAb("11");
                setAc("121");
                setAd("11");
                setAe("11");
                setAf("11");
                setAg("11");
                setMap(map);
            }});
            System.out.println("sourceClass = " + sourceClass);


            TestData targetClass = null;

            // 通过序列化实现深拷贝
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(bos);
            // 序列化以及传递这个对象
            oos.writeObject(sourceClass);
            oos.flush();
            ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
            ois = new ObjectInputStream(bin);
            // 返回新的对象
            targetClass = (TestData) ois.readObject();
            System.out.println("targetClass = " + targetClass);

            // 校验内容是否相同
            System.out.println("sourceClass.getData() == targetClass.getData() ? " + (sourceClass.getData() == targetClass.getData()));

            // 改变原始对象的内容
            System.out.println("\n -----------------  修改原始对象内容 ------------------");
            sourceClass.getData().setAg("12313");

            // 查看每一个现在的内容
            System.out.println("修改了原始对象后 sourceClass.getData().getAg() = " + sourceClass.getData().getAg());
            System.out.println("修改了原始对象后 targetClass.getData().getAg() = " + targetClass.getData().getAg());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(oos != null){
                oos.close();
            }
            if(ois != null){
                ois.close();
            }
        }
    }
}

测试结果:

参考文章:Java对象深度复制和序列化总结_0li0的博客-CSDN博客_java深度序列化 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值