浅谈BeanUtils的拷贝,深度克隆

原创
2018/11/30 14:09
阅读数 6.8W

1、BeanUtil本地简单测试
在项目中由于需要对某些对象进行深度拷贝然后进行持久化操作,想到了apache和spring都提供了BeanUtils的深度拷贝工具包,自己写了几个Demo做测试,定义了两个类User和Person,其中User的属性引用了Person类。

复制代码

public class User {
    private int id;
    private String username;// 用户姓名
    private String sex;// 性别
    private Date birthday;// 生日
    private String address;// 地址

    private Person person; //包装类

    //get set方法此处省略
}

复制代码

复制代码

//Person类
public class Person {
    private int id;
    private String userName ;
    private int age ;
    private String mobilePhone ;
    public  Person(){}
    public Person(int id,String userName, int age, String mobilePhone) {
        this.id = id;
        this.userName = userName;
        this.age = age;
        this.mobilePhone = mobilePhone;
    }

    //get set方法此处省略
}

复制代码

编写测试方法进行调研,主要是查看对象中包装的对象是否引用了同一个地址,从而判断是否是深度拷贝还是浅拷贝

复制代码

@Test
    public void CopyTest(){
        User user=new User();
        user.setId(1);
        user.setSex("man");
        user.setUsername("Tison");
        user.setAddress("address");
        user.setBirthday(new Date());
        Person p=new Person();
        p.setUserName("p1");
        user.setPerson(p);
        User target=new User();
        BeanUtils.copyProperties(user,target);
        System.out.println(target.getAddress()==user.getAddress());
        System.out.println(target.getPerson()==user.getPerson());
        System.out.println(user.toString());
        System.out.println(target.toString());
    }

复制代码

打印结果:

1

2

3

4

false  (String属性的内存地址不相等)

false  (包装对象的内存地址不相等)

src.main.mybatis.User@7907ec20

src.main.mybatis.User@546a03af

两个对象的哈希码不相等,引用对象的地址也不相同,并且对包装对象的操作都是互不影响,简单测试下可以看到BeanUtils实现了深度拷贝的效果。

2、项目测试
但是到了本人所做的项目中,BeanUtils的效果就不是深度拷贝了,用伪代码进行简单说明:

复制代码

//source为A对象,target为B对象
BeanUtils.copyProperties(source,target);
//调用setSecret方法将B对象的某型包装属性set为null
setSecret(target);
//分别打印对比的结果
System.out.println(target.getUserInfo()==source.getUserInfo());
System.out.println(source.getUserInfo().hashCode());
System.out.println(target.getUserInfo().hashCode());

复制代码

1

2

3

4

//打印测试结果

true

1589531316

1589531316

两份对象里的包装对象内存地址比较结果为true,而且对象的哈希吗指向了同一位置。
显而易见,BeanUtils并未进行深度拷贝。本人在项目中正因为采用了BeanUtils的拷贝方法,在对两份对象的不同操作时都会互相影响导致持久化的异常,可见基于BeanUtils的拷贝方法并不是万能的,而且由于源码中采用反射机制,其性能也被许多博主诟病,在网上进行了综合调研,发现BeanUtils的copyProperties()方法的确存在着浅拷贝的情况,这对于持久化操作实体类的时候是很大的一个坑,那么最靠谱的深拷贝方法还是要序列化后写流的方法,只是该方法需要实现Serializable接口。

3、深拷贝
深复制(深克隆)被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量,那些引用其他对象的变量将指向被复制过的新对象,而不再试原有的那些被引用的对象,换言之,深复制把要复制的对象所引用的对象都复制了一遍。
把对象写到流里的过程是串行化(Serilization)过程,但是在Java程序师圈子里又非常形象地称为“冷冻”或者“腌咸菜(picking)”过程;而把对象从流中读出来的并行化(Deserialization)过程则叫做“解冻”或者“回鲜(depicking)”过程。应当指出的是,写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面,因此“腌成咸菜”的只是对象的一个拷贝,Java咸菜还可以回鲜。在Java语言里深复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里(腌成咸菜),再从流里读出来(把咸菜回鲜),便可以重建对象。

在项目中我们需要克隆的对象可能包含多层引用类型,这就要涉及到多层克隆问题,多层克隆不仅要将克隆对象实现序列化接口,引用对象也同样的要实现序列化接口:

复制代码

public class User implements Serializable{
    private int id;
    private String username;// 用户姓名
    private String sex;// 性别
    private Date birthday;// 生日
    private String address;// 地址
    private Person person; //引用类型

    public User myColon(){
        User copy=null;
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(this);
            //将流序列化成对象
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            copy = (User) ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
            return copy;
    }

    //此处省略get-set方法代码
}

复制代码

引用类型也需要实现Serializable接口,否则会序列化失败。

复制代码

public class Person implements Serializable {
    private int id;
    private String userName ;
    private int age ;
    private String mobilePhone ;
    public  Person(){}
    public Person(int id,String userName, int age, String mobilePhone) {
        this.id = id;
        this.userName = userName;
        this.age = age;
        this.mobilePhone = mobilePhone;
    }
    //此处省略get-set方法
}

复制代码

结论:

1

2

3

结论:

1、BeanUtils的copyProperties()方法并不是完全的深度克隆,在包含有引用类型的对象拷贝上就可能会出现引用对象指向同一个的情况,且该方法的性能低下,项目中一定要谨慎使用。

2、要实现高性能且安全的深度克隆方法还是实现Serializable接口,多层克隆时,引用类型均要实现Serializable接口。

 

 

 

IV. 小结
1. 深拷贝和浅拷贝
深拷贝

相当于创建了一个新的对象,只是这个对象的所有内容,都和被拷贝的对象一模一样而已,即两者的修改是隔离的,相互之间没有影响 
- 完全独立

浅拷贝

也是创建了一个对象,但是这个对象的某些内容(比如A)依然是被拷贝对象的,即通过这两个对象中任意一个修改A,两个对象的A都会受到影响

等同与新创建一个对象,然后使用=,将原对象的属性赋值给新对象的属性
需要实现Cloneable接口
2. 对象拷贝的两种方法
通过反射方式实现对象拷贝

主要原理就是通过反射获取所有的属性,然后反射更改属性的内容

通过代理实现对象拷贝

将原SourceA拷贝到目标DestB

创建一个代理 copyProxy 
在代理中,依次调用 SourceA的get方法获取属性值,然后调用DestB的set方法进行赋值

展开阅读全文
打赏
0
2 收藏
分享
加载中
打赏
3 评论
2 收藏
0
分享
返回顶部
顶部