Java

对象拷贝

它们两种工具本质上就是对象拷贝工具,而对象拷贝又分为深拷贝和浅拷贝,下面进行详细解释。

浅拷贝和深拷贝

在Java中,除了 基本数据类型之外,还存在 类的实例对象这个引用数据类型,而一般使用 “=”号做赋值操作的时候,对于基本数据类型,实际上是拷贝的它的值,但是对于对象而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,他们实际还是指向的同一个对象。
而浅拷贝和深拷贝就是在这个基础上做的区分,如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝。
简单来说:

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

    BeanUtils

    Apache 的 BeanUtils

    BeanUtils的例子
    1. public class PersonSource {
    2. private Integer id;
    3. private String username;
    4. private String password;
    5. private Integer age;
    6. // getters/setters omiited
    7. }
    8. public class PersonDest {
    9. private Integer id;
    10. private String username;
    11. private Integer age;
    12. // getters/setters omiited
    13. }
    14. public class TestApacheBeanUtils {
    15. public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
    16. //下面只是用于单独测试
    17. PersonSource personSource = new PersonSource(1, "pjmike", "12345", 21);
    18. PersonDest personDest = new PersonDest();
    19. BeanUtils.copyProperties(personDest,personSource);
    20. System.out.println("persondest: "+personDest);
    21. }
    22. }
    23. persondest: PersonDest{id=1, username='pjmike', age=21}
    从上面的例子可以看出,对象拷贝非常简单,BeanUtils最常用的方法就是:
    1. //将源对象中的值拷贝到目标对象
    2. public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
    3. BeanUtilsBean.getInstance().copyProperties(dest, orig);
    4. }
    默认情况下,使用org.apache.commons.beanutils.BeanUtils对复杂对象的复制是引用,这是一种浅拷贝
    但是由于 Apache下的BeanUtils对象拷贝性能太差,不建议使用,而且在阿里巴巴Java开发规约插件上也明确指出:

    Ali-Check | 避免用Apache Beanutils进行属性的copy。

commons-beantutils 对于对象拷贝加了很多的检验,包括类型的转换,甚至还会检验对象所属的类的可访问性,可谓相当复杂,这也造就了它的差劲的性能,具体实现代码如下:

  1. public void copyProperties(final Object dest, final Object orig)
  2. throws IllegalAccessException, InvocationTargetException {
  3. // Validate existence of the specified beans
  4. if (dest == null) {
  5. throw new IllegalArgumentException
  6. ("No destination bean specified");
  7. }
  8. if (orig == null) {
  9. throw new IllegalArgumentException("No origin bean specified");
  10. }
  11. if (log.isDebugEnabled()) {
  12. log.debug("BeanUtils.copyProperties(" + dest + ", " +
  13. orig + ")");
  14. }
  15. // Copy the properties, converting as necessary
  16. if (orig instanceof DynaBean) {
  17. final DynaProperty[] origDescriptors =
  18. ((DynaBean) orig).getDynaClass().getDynaProperties();
  19. for (DynaProperty origDescriptor : origDescriptors) {
  20. final String name = origDescriptor.getName();
  21. // Need to check isReadable() for WrapDynaBean
  22. // (see Jira issue# BEANUTILS-61)
  23. if (getPropertyUtils().isReadable(orig, name) &&
  24. getPropertyUtils().isWriteable(dest, name)) {
  25. final Object value = ((DynaBean) orig).get(name);
  26. copyProperty(dest, name, value);
  27. }
  28. }
  29. } else if (orig instanceof Map) {
  30. @SuppressWarnings("unchecked")
  31. final
  32. // Map properties are always of type <String, Object>
  33. Map<String, Object> propMap = (Map<String, Object>) orig;
  34. for (final Map.Entry<String, Object> entry : propMap.entrySet()) {
  35. final String name = entry.getKey();
  36. if (getPropertyUtils().isWriteable(dest, name)) {
  37. copyProperty(dest, name, entry.getValue());
  38. }
  39. }
  40. } else /* if (orig is a standard JavaBean) */ {
  41. final PropertyDescriptor[] origDescriptors =
  42. getPropertyUtils().getPropertyDescriptors(orig);
  43. for (PropertyDescriptor origDescriptor : origDescriptors) {
  44. final String name = origDescriptor.getName();
  45. if ("class".equals(name)) {
  46. continue; // No point in trying to set an object's class
  47. }
  48. if (getPropertyUtils().isReadable(orig, name) &&
  49. getPropertyUtils().isWriteable(dest, name)) {
  50. try {
  51. final Object value =
  52. getPropertyUtils().getSimpleProperty(orig, name);
  53. copyProperty(dest, name, value);
  54. } catch (final NoSuchMethodException e) {
  55. // Should not happen
  56. }
  57. }
  58. }
  59. }
  60. }

Spring的 BeanUtils

使用Spring的BeanUtils进行对象拷贝:

  1. public class TestSpringBeanUtils {
  2. public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
  3. //下面只是用于单独测试
  4. PersonSource personSource = new PersonSource(1, "pjmike", "12345", 21);
  5. PersonDest personDest = new PersonDest();
  6. BeanUtils.copyProperties(personSource,personDest);
  7. System.out.println("persondest: "+personDest);
  8. }
  9. }

Spring下的BeanUtils也是使用 copyProperties方法进行拷贝,只不过它的实现方式非常简单,就是对两个对象中相同名字的属性进行简单的get/set,仅检查属性的可访问性。
去源码里面:

  • 版本:spring-benas-4.3.8.RELEASE
  • 方法:org.springframework.beans.BeanUtils#copyProperties(java.lang.Object, java.lang.Object, java.lang.Class<?>, java.lang.String...)

    1. private static void copyProperties(Object source, Object target, @Nullable Class<?> editable, @Nullable String... ignoreProperties) throws BeansException {
    2. Assert.notNull(source, "Source must not be null");
    3. Assert.notNull(target, "Target must not be null");
    4. Class<?> actualEditable = target.getClass();
    5. if (editable != null) {
    6. if (!editable.isInstance(target)) {
    7. throw new IllegalArgumentException("Target class [" + target.getClass().getName() + "] not assignable to Editable class [" + editable.getName() + "]");
    8. }
    9. actualEditable = editable;
    10. }
    11. PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
    12. List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
    13. PropertyDescriptor[] var7 = targetPds;
    14. int var8 = targetPds.length;
    15. for(int var9 = 0; var9 < var8; ++var9) {
    16. PropertyDescriptor targetPd = var7[var9];
    17. Method writeMethod = targetPd.getWriteMethod();
    18. if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
    19. PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
    20. if (sourcePd != null) {
    21. Method readMethod = sourcePd.getReadMethod();
    22. if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
    23. try {
    24. if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
    25. readMethod.setAccessible(true);
    26. }
    27. Object value = readMethod.invoke(source);
    28. if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
    29. writeMethod.setAccessible(true);
    30. }
    31. writeMethod.invoke(target, value);
    32. } catch (Throwable var15) {
    33. throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var15);
    34. }
    35. }
    36. }
    37. }
    38. }
    39. }

    这个方法的源码其实很短,只有 44 行,我给大家把关键的地方写上注释,截图如下:
    2021-05-22-16-47-06-848031.png
    可以看到,成员变量赋值是基于目标对象的成员列表,并且会跳过ignore的以及在源对象中不存在,所以这个方法是安全的,不会因为两个对象之间的结构差异导致错误,但是必须保证同名的两个成员变量类型相同
    所以从上面的源码解读中我可以得到这样的几条结论:

  1. 对于要复制的属性,目标对象必须要有对应的 set 方法(上图的第 27 行),源对象必须要有对应的 get 方法(上图的第 34 行)。
  2. 对于要复制的属性,目标对象和源对象的属性名称得一模一样。(上图的第 34 行)。
  3. 目标对象对应的 set 方法的入参和源对象对应的 get 方法的返回类型必须得一致(上图的第 37 行)。

    Spring 的 BeanUtils 的 CopyProperties 方法拷贝内部类的时候有问题

    内部类其实就是 Java 的一颗语法糖而已。
    比如常用的自动拆箱、自动装箱、高级 for 循环、泛型等等,包括 JDK 10 那个不三不四的局部变量类型推断功能,就是那个 var,也是语法糖而已。
    那么内部类这颗语法糖长什么样子呢?
    2021-05-22-16-47-06-587701.png
    现在先记住这个几个 class 类。
    通过代码来验证一下内部类拷贝时的问题:
    两个对象 CopyTest1 和 CopyTest2,对象的结构和里面的属性看起来是一模一样的:

    1. @ToString
    2. @Data
    3. public class CopyTest1 {
    4. public String outerName;
    5. public CopyTest1.InnerClass innerClass;
    6. public List<CopyTest1.InnerClass> clazz;
    7. @ToString
    8. @Data
    9. public static class InnerClass {
    10. public String InnerName;
    11. }
    12. }
    1. @ToString
    2. @Data
    3. public class CopyTest2 {
    4. public String outerName;
    5. public CopyTest2.InnerClass innerClass;
    6. public List<CopyTest2.InnerClass> clazz;
    7. @ToString
    8. @Data
    9. public static class InnerClass {
    10. public String InnerName;
    11. }
    12. }
    1. public class MainTest {
    2. public static void main(String[] args) {
    3. CopyTest1 test1 = new CopyTest1();
    4. test1.outerName = "hahaha";
    5. CopyTest1.InnerClass innerClass = new CopyTest1.InnerClass();
    6. innerClass.InnerName = "hohoho";
    7. test1.innerClass = innerClass;
    8. System.out.println(test1.toString());
    9. CopyTest2 test2 = new CopyTest2();
    10. BeanUtils.copyProperties(test1, test2);
    11. System.out.println(test2.toString());
    12. }
    13. }

    来验证一下。
    2021-05-22-16-47-06-965442.png
    源对象 set 方法的入参是 CopyTest2$InnerClass,而目标对象 get 方法的返参是 CopyTest1$InnerClass
    不满足前面总结出来的第三点,所以不会拷贝成功。
    解决方案是单独设置一下内部类,单独 copy。
    如果内部类的 bean 属性较多或者递归的 bean 属性很多,那可以自己封装一个方法,用于递归拷贝,这里只有一层,所以直接额外 copy 一次。

    1. public class MainTest {
    2. public static void main(String[] args) {
    3. CopyTest1 test1 = new CopyTest1();
    4. test1.outerName = "hahaha";
    5. CopyTest1.InnerClass innerClass = new CopyTest1.InnerClass();
    6. innerClass.InnerName = "hohoho";
    7. test1.innerClass = innerClass;
    8. System.out.println(test1.toString());
    9. CopyTest2 test2 = new CopyTest2();
    10. test2.innerClass = new CopyTest2.InnerClass();
    11. BeanUtils.copyProperties(test1, test2);
    12. BeanUtils.copyProperties(test1.innerClass, test2.innerClass);
    13. System.out.println(test2.toString());
    14. }
    15. }

    2021-05-22-16-47-06-460043.png

    小结

    以上简要的分析两种BeanUtils,因为Apache下的BeanUtils性能较差,不建议使用,可以使用 Spring的BeanUtils,或者使用其他拷贝框架,比如 cglib BeanCopier,基于javassist的Orika等,这些也是非常优秀的类库,值得去尝试。