java 对象拷贝_Java利器之对象拷贝

一、引言

1.场景:当从后台接口中获取一个ResponseData时,除了将信息展示给用户看,因业务需要,有时ResponseData还需要在别的地方进行使用,这时候就需要对对象进行copy,来做相应的业务处理。为了简单起见,这里假设两个对象factory1 和 factory2 ,两者都是Factory类 的对象,有两个成员变量。

public class Factory{

int income;

Worker worker;

}

现将对象factory1 拷贝赋值给factory2,也就是

factory2.income = factory1.income;

factory2.worker = factory1.worker;

这时,如果再去修改factory1的引用变量属性worker值时候,会遇到将factory2的worker值也同步修改了的问题,而修改factory1的income时,则不会影响factory2的属性值。

0b0daddc26ea

image

这又是什么骚操作?

这里边涉及到了值传递和引用传递的问题。Java数据类型分为基本数据类型和引用数据类型。基本数据类型的赋值是将值进行传递,而引用类型则是将对象的内存地址传递给factory2,所以在修改factory1的引用类型数据时,因为两个对象共用一个内存地址,所以factory2的值也会发生改变。

这就是所谓的对象拷贝。

2.定义:对象拷贝(Object Copy)就是将一个对象的属性拷贝到另一个有着相同类类型的对象中去。在程序中,拷贝是很常见的,主要用于在新的上下文环境中复用对象的部分或全部数据。Java中有三种类型的对象拷贝:浅拷贝(Shallow Copy)、深拷贝(Deep Copy)、延迟拷贝(Lazy Copy)。

二、浅拷贝(Shallow Copy)

1.什么是浅拷贝

​ 浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性值是内存地址(引用类型),拷贝的就是内存地址。因此,如果其中一个对象改变了这个地址内的值,就会影响到另一个对象。

0b0daddc26ea

image

在上图中,SourceObject有一个int类型的属性"field1"和一个引用类型属性"refObj"。当对SourceObject做浅拷贝时,创建了CopiedObject,它有一个包含"field1"拷贝值的属性"field2"以及仍指向“refObj”本身的引用。由于field1是基本类型,所以只是将它的值拷贝给"field2",但是由于“refObj”是一个引用类型,所以CopiedObject指向"refObj"相同的地址。因此对SourceObject中的"refObj"所做的任何改变都会影响到CopiedObject。

2.如何实现浅拷贝

下面是实现浅拷贝的一个例子

public class Subject {

private String name;

public Subject(String name) {

this.name = name;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

}

public class Student implements Cloneable{

/**

* 对象引用

*/

private Subject subject;

private String name;

public Student(String name,String subjectName) {

this.name = name;

this.subject = new Subject(subjectName);

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public Subject getSubject() {

return subject;

}

/**

* 重写clone()方法

* @return

*/

@Override

protected Object clone(){

try {

return super.clone();

}catch (CloneNotSupportedException e){

return null;

}

}

}

咱们对Student对象进行拷贝,看下结果如何?

前方高能,请注意!!!

public class CopyTest {

public static void main(String[] args) {

Student student = new Student("Lucy","语文");

System.out.println("Original Object: " + student.getName()

+ " - " + student.getSubject().getName());

//拷贝对象

Student clonedStudent = (Student) student.clone();

System.out.println("Cloned Object: " + clonedStudent.getName()

+ " - " + clonedStudent.getSubject().getName());

//原始对象和copy对象是否一样

System.out.println("Is Original object the same with Cloned Object: "+(student == clonedStudent));

//原始对象和拷贝对象的那么属性是否一样

System.out.println("Is Original Object's field name the same with Cloned Object: "+ (student.getName() == clonedStudent.getName()));

//原始对象和拷贝对象的subject 属性是否一样

System.out.println("Is Original Object's field subject the same with Cloned Object: " + (student.getSubject() == clonedStudent.getSubject()));

student.setName("Ben");

student.getSubject().setName("物理");

System.out.println("Original Object after it is updated: "

+ student.getName() + " - " + student.getSubject().getName());

System.out.println("Cloned Object after updating original object: "

+ clonedStudent.getName() + " - " + clonedStudent.getSubject().getName());

}

}

输出结果如下:

Original Object: Lucy - 语文

Cloned Object: Lucy - 语文

Is Original object the same with Cloned Object: false

Is Original Object's field name the same with Cloned Object: true

Is Original Object's field subject the same with Cloned Object: true

Original Object after it is updated: Ben - 物理

Cloned Object after updating original object: Lucy - 物理

在这个例子中,我让要拷贝的类Student实现了Cloneable接口并重写了Object类的clone()方法,然后在方法内部调用了super.clone()方法。从输出结果可以看出,对原始对象student的name属性所做的改变并没有影响到拷贝对象的clonedStudent,但引用对象的subject的name属性所做的改变影响到了拷贝对象clonedStudent。

这里可能就有人提出反驳了,What??? 那么 String类型也不是基本数据类型,它也是个类,也是引用类型啊,它怎么没有像subject一样改变?

老兄,别着急! 容我喝口水,慢慢道来。熟悉String的大佬都知道,String类型是不可变类型,当你尝试对String类型的值进行修改时,其实,它实际上是创建了一个新的String对象,并将以引用指向了新的对象。 这里不是咱们的重点,咱们接着往下说。

如此一来,有很多小伙伴又有了新的问题,这种问题由于业务需要,又该怎样解决呢?那就不得不提到另一个名词——深拷贝

三、深拷贝(Deep Copy)

1.什么是深拷贝?

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时,即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。

图示为深拷贝

[图片上传失败...(image-918c2a-1588509170671)]

在上图中,SourceObject有一个int类型的属性"field1"和一个引用类型属性"refObj1"。当对SourceObject 做深拷贝时,创建了CopiedObject,它有一个包含"filed1"拷贝值的属性"field2"以及包含"refObj1"拷贝值的引用类型属性"refObj2"。 因此,对SourceObject中的"refObj"所做的任何改变都不会影响到CopiedObject。

2.如何实现深拷贝?

下面是实现深拷贝的一个例子。只是在浅拷贝例子的基础上做了一点小改动,Subject和CopyTest都没有变化。

public class Student implements Cloneable{

/**

* 对象引用

*/

private Subject subject;

private String name;

public Student(String name,String subjectName) {

this.name = name;

this.subject = new Subject(subjectName);

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public Subject getSubject() {

return subject;

}

/**

* 重写clone()方法

* @return

*/

@Override

protected Object clone(){

Student student = new Student(name,getSubject().getName());

return student;

}

}

输出结果如下:

Original Object: Lucy - 语文

Cloned Object: Lucy - 语文

Is Original object the same with Cloned Object: false

Is Original Object's field name the same with Cloned Object: true

Is Original Object's field subject the same with Cloned Object: false

Original Object after it is updated: Ben - 物理

Cloned Object after updating original object: Lucy - 语文

很容易发现clone()方法中的一点变化。因为是深拷贝,所以你需要创建一个拷贝类的对象。同时Subject也会创建一个新的对象,分配了新的内存空间,而不再指向同一个引用地址。当然,实现的前提是在Student类中实现cloneable接口,并重写clone()方法。

2.通过递归拷贝,实现深拷贝

当一个类中有引用类型的变量时,在clone()时,同时对引用类型变量也进行拷贝,实现深拷贝的目的。

CopyTest 无需改变,需要对引用类型Subject实现Cloneable,并重写clone()方法。话不多说,上代码。

public class Subject implements Cloneable{

private String name;

public Subject(String name) {

this.name = name;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

@Override

protected Object clone() throws CloneNotSupportedException {

//Subject如果也有引用类型的成员属性,应该和Student类一样实现

return super.clone();

}

@Override

public String toString() {

return "Subject{" +

"name='" + name + '\'' +

'}';

}

}

public class Student implements Cloneable{

/**

* 对象引用类型

*/

private Subject subject;

private String name;

public Student(String name,String subjectName) {

this.name = name;

this.subject = new Subject(subjectName);

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public Subject getSubject() {

return subject;

}

/**

* 深拷贝

* 重写clone()方法

* @return

*/

@Override

protected Object clone(){

try {

Student student = (Student) super.clone();

student.subject = (Subject) subject.clone();

return student;

}catch (CloneNotSupportedException e){

return null;

}

}

}

输出结果如下:

Original Object: Lucy - 语文

Cloned Object: Lucy - 语文

Is Original object the same with Cloned Object: false

Is Original Object's field name the same with Cloned Object: true

Is Original Object's field subject the same with Cloned Object: false

Original Object after it is updated: Ben - 物理

Cloned Object after updating original object: Lucy - 语文

3.通过序列化实现深拷贝

可以通过序列化实现深拷贝。那么,序列化是做什么的呢?它将整个对象图写入持久化存储文件中并且当需要的时候把它读回来,这意味着当你需要把它读回来时,你需要整个对象图的拷贝。这就是当你深拷贝一个对象时真正需要的东西。序列化拷贝实现的前提条件是确保对象图中的所有类都是可序列化的。

我们还是使用上面的例子,Subject类和Student都实现了序列化Serializable接口,没有太大变化。

public class Subject implements Serializable {

private String name;

public Subject(String name) {

this.name = name;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

@Override

public String toString() {

return "Subject{" +

"name='" + name + '\'' +

'}';

}

}

public class Student implements Serializable {

/**

* 对象引用

*/

private Subject subject;

private String name;

public Student(String name,String subjectName) {

this.name = name;

this.subject = new Subject(subjectName);

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public Subject getSubject() {

return subject;

}

@Override

public String toString() {

return "Student{" +

"subject=" + subject +

", name='" + name + '\'' +

'}';

}

}

//重点是CopyTest的序列化与反序列化的处理

public class CopyTest {

public static void main(String[] args) {

ObjectOutputStream oos = null;

ObjectInputStream ois = null;

ByteArrayOutputStream baos = null;

ByteArrayInputStream bais = null;

try {

//创建原始的可序列化对象

Student originalStudent = new Student("Lucy","语文");

System.out.println("Original Object = " + originalStudent);

Student copiedStudent = null;

//通过序列化实现深拷贝

baos = new ByteArrayOutputStream();

oos = new ObjectOutputStream(baos);

//序列化及传递这个对象

oos.writeObject(originalStudent);

oos.flush();

bais = new ByteArrayInputStream(baos.toByteArray());

ois = new ObjectInputStream(bais);

//返回新的对象

copiedStudent = (Student) ois.readObject();

//校验内容是否一致

System.out.println("Copied Object = "+ copiedStudent);

//改变原始对象的内容

originalStudent.setName("Ben");

originalStudent.getSubject().setName("化学");

System.out.println("Original Object = "+ originalStudent);

System.out.println("Copied Object = "+ copiedStudent);

}catch (Exception e){

System.out.println("error: "+ e.getMessage());

}finally {

if (baos != null){

try {

baos.close();

} catch (IOException e) {

e.printStackTrace();

}

}

if (bais != null){

try {

bais.close();

} catch (IOException e) {

e.printStackTrace();

}

}

if (oos != null){

try {

oos.close();

} catch (IOException e) {

e.printStackTrace();

}

}

if (ois != null){

try {

ois.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

}

}

输出结果如下:

Original Object = Student{subject=Subject{name='语文'}, name='Lucy'}

Copied Object = Student{subject=Subject{name='语文'}, name='Lucy'}

Original Object = Student{subject=Subject{name='化学'}, name='Ben'}

Copied Object = Student{subject=Subject{name='语文'}, name='Lucy'}

这里,实现序列化拷贝需要以下几步:

确保对象图中所有类都是可序列化的;

创建输入输出流;

创建对象输入输出流;

将你要拷贝的对象传递给对象输出流;

从对象输入流中读取新的对象,并转换成相同类的对象

敲黑板,注意啦!!!

注意事项:

1.这种方式无法序列化transient变量,因此无法拷贝transient变量.

2.比较消耗性能。 创建一个socket,序列化一个对象,通过socket传输它,然后反序列化它,这个过程与调用已有对象的方法相比是很慢的的,在性能表现上较差。因此,如果你对性能要求较高,不推荐你使用这种方式。

总结:

三种深拷贝方式中,我个人是更倾向于第一种方式的,简单明了,无需对涉及到的所有类都处理clone()操作。当然,其他两种方式也都可以,看自己的选择了。

四、延迟拷贝(Lazy Copy)

延迟拷贝时浅拷贝和深拷贝的一个组合,实(可)际(以)上(用)很(来)少(装)使(逼)用。当最开始拷贝一个对象时,会使用速度较快的浅拷贝,还会使用一个计数器来记录有多少对象共享这个数据。当程序想要修改原始的对象时,它会决定数据是否会被共享(通过检查计数器),并根据需要进行深拷贝。

延迟拷贝从外面看就是深拷贝,但是只要有可能就会利用浅拷贝的速度。当原始对象中的引用不经常改变的时候可以使用延迟拷贝。由于存在计数器,效率下降很大。但只是常量级的开销。而且,某些情况下,循环引用会导致一些问题。

五、如何选择

如果对象的属性全是基本数据类型,则使用浅拷贝;

如果对象属性有引用数据类型,需要基于具体的需求进行选择浅拷贝还是深拷贝。如果对象引用任何时候都不会改变,则只需要浅拷贝即可。否则,就需要选择深拷贝。基于自己的需求场景来决定。


版权声明:本文为weixin_34861702原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。