面试问题总结之java 深拷贝浅拷贝

本文详细介绍了Java中对象拷贝的概念,包括浅拷贝和深拷贝的区别与实现方法。通过具体示例代码展示了如何实现这两种拷贝方式,并讨论了序列化作为实现深拷贝的一种替代方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

自己写的不用别人总结的,粘贴过来,留着自己看,其实仔细看看下面的那两个图就ok了

1.原文链接http://www.jb51.net/article/48201.htm

2.海子的http://www.cnblogs.com/dolphin0520/p/3700693.html

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

二、浅拷贝

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


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

2、如何实现浅拷贝

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

复制代码代码如下:

public class Subject {
private String name;
public Subject(String s) {
name = s;
}
public String getName() {
return name;
}
public void setName(String s) {
name = s;
}
}
public class Student implements Cloneable {
// 对象引用
private Subject subj;

private String name;

public Student(String s, String sub) {
name = s;
subj = new Subject(sub);
}

public Subject getSubj() {
return subj;
}

public String getName() {
return name;
}

public void setName(String s) {
name = s;
}

/**
* 重写clone()方法
* @return
*/
public Object clone() {
//浅拷贝
try {
// 直接调用父类的clone()方法
return super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
}
public class CopyTest {
public static void main(String[] args) {
// 原始对象
Student stud = new Student("John", "Algebra");
System.out.println("Original Object: " + stud.getName() + " - " + stud.getSubj().getName());
// 拷贝对象
Student clonedStud = (Student) stud.clone();
System.out.println("Cloned Object: " + clonedStud.getName() + " - " + clonedStud.getSubj().getName());
// 原始对象和拷贝对象是否一样:
System.out.println("Is Original Object the same with Cloned Object: " + (stud == clonedStud));
// 原始对象和拷贝对象的name属性是否一样
System.out.println("Is Original Object's field name the same with Cloned Object: " +
(stud.getName() == clonedStud.getName()));
// 原始对象和拷贝对象的subj属性是否一样
System.out.println("Is Original Object's field subj the same with Cloned Object: " +
(stud.getSubj() == clonedStud.getSubj()));
stud.setName("Dan");
stud.getSubj().setName("Physics");
System.out.println("Original Object after it is updated: " + stud.getName() + " - " +
stud.getSubj().getName());
System.out.println("Cloned Object after updating original object: " + clonedStud.getName() +
" - " + clonedStud.getSubj().getName());
}
}

输出结果如下:
Original Object: John - Algebra
Cloned Object: John - Algebra
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 subj the same with Cloned Object: true
Original Object after it is updated: Dan - Physics
Cloned Object after updating original object: John - Physics

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

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



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

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

复制代码代码如下:

public class Student implements Cloneable {
// 对象引用
private Subject subj;

private String name;

public Student(String s, String sub) {
name = s;
subj = new Subject(sub);
}

public Subject getSubj() {
return subj;
}

public String getName() {
return name;
}

public void setName(String s) {
name = s;
}

/**
* 重写clone()方法
*
* @return
*/
public Object clone() {
// 深拷贝,创建拷贝类的一个新对象,这样就和原始对象相互独立
Student s = new Student(name, subj.getName());
return s;
}
}

输出结果如下:
Original Object: John - Algebra
Cloned Object: John - Algebra
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 subj the same with Cloned Object: false
Original Object after it is updated: Dan - Physics
Cloned Object after updating original object: John - Algebra

很容易发现clone()方法中的一点变化。因为它是深拷贝,所以你需要创建拷贝类的一个对象。因为在Student类中有对象引用,所以需要在Student类中实现Cloneable接口并且重写clone方法。

3、通过序列化实现深拷贝
也可以通过序列化来实现深拷贝。序列化是干什么的?它将整个对象图写入到一个持久化存储文件中并且当需要的时候把它读取回来, 这意味着当你需要把它读取回来时你需要整个对象图的一个拷贝。这就是当你深拷贝一个对象时真正需要的东西。请注意,当你通过序列化进行深拷贝时,必须确保对象图中所有类都是可序列化的。

复制代码代码如下:

public class ColoredCircle implements Serializable {
private int x;
private int y;

public ColoredCircle(int x, int y) {
this.x = x;
this.y = y;
}

public int getX() {
return x;
}

public void setX(int x) {
this.x = x;
}

public int getY() {
return y;
}

public void setY(int y) {
this.y = y;
}

@Override
public String toString() {
return "x=" + x + ", y=" + y;
}
}
public class DeepCopy {

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

try {
// 创建原始的可序列化对象
ColoredCircle c1 = new ColoredCircle(100, 100);
System.out.println("Original = " + c1);

ColoredCircle c2 = null;

// 通过序列化实现深拷贝
ByteArrayOutputStream bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
// 序列化以及传递这个对象
oos.writeObject(c1);
oos.flush();
ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bin);
// 返回新的对象
c2 = (ColoredCircle) ois.readObject();

// 校验内容是否相同
System.out.println("Copied = " + c2);
// 改变原始对象的内容
c1.setX(200);
c1.setY(200);
// 查看每一个现在的内容
System.out.println("Original = " + c1);
System.out.println("Copied = " + c2);
} catch (Exception e) {
System.out.println("Exception in main = " + e);
} finally {
oos.close();
ois.close();
}
}
}

输出结果如下:
Original = x=100, y=100
Copied = x=100, y=100
Original = x=200, y=200
Copied = x=100, y=100
这里,你只需要做以下几件事儿:
(1)确保对象图中的所有类都是可序列化的
(2)创建输入输出流
(3)使用这个输入输出流来创建对象输入和对象输出流
(4)将你想要拷贝的对象传递给对象输出流
(5)从对象输入流中读取新的对象并且转换回你所发送的对象的类

在这个例子中,我创建了一个ColoredCircle对象c1然后将它序列化 (将它写到ByteArrayOutputStream中). 然后我反序列化这个序列化后的对象并将它保存到c2中。随后我修改了原始对象c1。然后结果如你所见,c1不同于c2,对c1所做的任何修改都不会影响c2。

注意,序列化这种方式有其自身的限制和问题:
因为无法序列化transient变量, 使用这种方法将无法拷贝transient变量。

再就是性能问题。创建一个socket, 序列化一个对象, 通过socket传输它, 然后反序列化它,这个过程与调用已有对象的方法相比是很慢的。所以在性能上会有天壤之别。如果性能对你的代码来说是至关重要的,建议不要使用这种方式。它比通过实现Clonable接口这种方式来进行深拷贝几乎多花100倍的时间。


假如说你想复制一个简单变量。很简单:

  1. int apples =5;
  2. int pears = apples;
		int apples = 5;
		int pears = apples;

不仅仅是int类型,其它七种原始数据类型(boolean,char,byte,short,float,double.long)同样适用于该类情况。

但是如果你复制的是一个对象,情况就有些复杂了。

假设说我是一个beginner,我会这样写:

  1. class Student {
  2. privateint number;
  3. publicint getNumber() {
  4. return number;
  5. }
  6. publicvoid setNumber(int number) {
  7. this.number = number;
  8. }
  9. }
  10. publicclass Test {
  11. publicstaticvoid main(String args[]) {
  12. Student stu1 =new Student();
  13. stu1.setNumber(12345);
  14. Student stu2 = stu1;
  15. System.out.println("学生1:" + stu1.getNumber());
  16. System.out.println("学生2:" + stu2.getNumber());
  17. }
  18. }
class Student {
	private int number;

	public int getNumber() {
		return number;
	}

	public void setNumber(int number) {
		this.number = number;
	}
	
}
public class Test {
	
	public static void main(String args[]) {
		
		Student stu1 = new Student();
		stu1.setNumber(12345);
		Student stu2 = stu1;
		
		System.out.println("学生1:" + stu1.getNumber());
		System.out.println("学生2:" + stu2.getNumber());
	}
}

打印结果:

  1. 学生1:12345
  2. 学生2:12345
学生1:12345
学生2:12345

这里我们自定义了一个学生类,该类只有一个number字段。

我们新建了一个学生实例,然后将该值赋值给stu2实例。(Student stu2 = stu1;)

再看看打印结果,作为一个新手,拍了拍胸腹,对象复制不过如此,

难道真的是这样吗?

我们试着改变stu2实例的number字段,再打印结果看看:

  1. stu2.setNumber(54321);
  2. System.out.println("学生1:" + stu1.getNumber());
  3. System.out.println("学生2:" + stu2.getNumber());
		stu2.setNumber(54321);
	
		System.out.println("学生1:" + stu1.getNumber());
		System.out.println("学生2:" + stu2.getNumber());

打印结果:

  1. 学生1:54321
  2. 学生2:54321
学生1:54321
学生2:54321

这就怪了,为什么改变学生2的学号,学生1的学号也发生了变化呢?

原因出在(stu2 = stu1) 这一句。该语句的作用是将stu1的引用赋值给stu2,

这样,stu1和stu2指向内存堆中同一个对象。如图:

那么,怎样才能达到复制一个对象呢?

是否记得万类之王Object。它有11个方法,有两个protected的方法,其中一个为clone方法。

该方法的签名是:

protected native Object clone() throws CloneNotSupportedException;

因为每个类直接或间接的父类都是Object,因此它们都含有clone()方法,但是因为该方法是protected,所以都不能在类外进行访问。

要想对一个对象进行复制,就需要对clone方法覆盖。

一般步骤是(浅复制):

1. 被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常) 该接口为标记接口(不含任何方法)

2. 覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象,(native为本地方法)

下面对上面那个方法进行改造:

  1. class Studentimplements Cloneable{
  2. privateint number;
  3. publicint getNumber() {
  4. return number;
  5. }
  6. publicvoid setNumber(int number) {
  7. this.number = number;
  8. }
  9. @Override
  10. public Object clone() {
  11. Student stu =null;
  12. try{
  13. stu = (Student)super.clone();
  14. }catch(CloneNotSupportedException e) {
  15. e.printStackTrace();
  16. }
  17. return stu;
  18. }
  19. }
  20. publicclass Test {
  21. publicstaticvoid main(String args[]) {
  22. Student stu1 =new Student();
  23. stu1.setNumber(12345);
  24. Student stu2 = (Student)stu1.clone();
  25. System.out.println("学生1:" + stu1.getNumber());
  26. System.out.println("学生2:" + stu2.getNumber());
  27. stu2.setNumber(54321);
  28. System.out.println("学生1:" + stu1.getNumber());
  29. System.out.println("学生2:" + stu2.getNumber());
  30. }
  31. }
class Student implements Cloneable{
	private int number;

	public int getNumber() {
		return number;
	}

	public void setNumber(int number) {
		this.number = number;
	}
	
	@Override
	public Object clone() {
		Student stu = null;
		try{
			stu = (Student)super.clone();
		}catch(CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return stu;
	}
}
public class Test {
	
	public static void main(String args[]) {
		
		Student stu1 = new Student();
		stu1.setNumber(12345);
		Student stu2 = (Student)stu1.clone();
		
		System.out.println("学生1:" + stu1.getNumber());
		System.out.println("学生2:" + stu2.getNumber());
		
		stu2.setNumber(54321);
	
		System.out.println("学生1:" + stu1.getNumber());
		System.out.println("学生2:" + stu2.getNumber());
	}
}

打印结果:

  1. 学生1:12345
  2. 学生2:12345
  3. 学生1:12345
  4. 学生2:54321
学生1:12345
学生2:12345
学生1:12345
学生2:54321

如果你还不相信这两个对象不是同一个对象,那么你可以看看这一句:

  1. System.out.println(stu1 == stu2);// false
		System.out.println(stu1 == stu2); // false

上面的复制被称为浅复制(Shallow Copy),还有一种稍微复杂的深度复制(deep copy):

我们在学生类里再加一个Address类。

  1. class Address {
  2. private String add;
  3. public String getAdd() {
  4. return add;
  5. }
  6. publicvoid setAdd(String add) {
  7. this.add = add;
  8. }
  9. }
  10. class Studentimplements Cloneable{
  11. privateint number;
  12. private Address addr;
  13. public Address getAddr() {
  14. return addr;
  15. }
  16. publicvoid setAddr(Address addr) {
  17. this.addr = addr;
  18. }
  19. publicint getNumber() {
  20. return number;
  21. }
  22. publicvoid setNumber(int number) {
  23. this.number = number;
  24. }
  25. @Override
  26. public Object clone() {
  27. Student stu =null;
  28. try{
  29. stu = (Student)super.clone();
  30. }catch(CloneNotSupportedException e) {
  31. e.printStackTrace();
  32. }
  33. return stu;
  34. }
  35. }
  36. publicclass Test {
  37. publicstaticvoid main(String args[]) {
  38. Address addr =new Address();
  39. addr.setAdd("杭州市");
  40. Student stu1 =new Student();
  41. stu1.setNumber(123);
  42. stu1.setAddr(addr);
  43. Student stu2 = (Student)stu1.clone();
  44. System.out.println("学生1:" + stu1.getNumber() +",地址:" + stu1.getAddr().getAdd());
  45. System.out.println("学生2:" + stu2.getNumber() +",地址:" + stu2.getAddr().getAdd());
  46. }
  47. }
class Address  {
	private String add;

	public String getAdd() {
		return add;
	}

	public void setAdd(String add) {
		this.add = add;
	}
	
}

class Student implements Cloneable{
	private int number;

	private Address addr;
	
	public Address getAddr() {
		return addr;
	}

	public void setAddr(Address addr) {
		this.addr = addr;
	}

	public int getNumber() {
		return number;
	}

	public void setNumber(int number) {
		this.number = number;
	}
	
	@Override
	public Object clone() {
		Student stu = null;
		try{
			stu = (Student)super.clone();
		}catch(CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return stu;
	}
}
public class Test {
	
	public static void main(String args[]) {
		
		Address addr = new Address();
		addr.setAdd("杭州市");
		Student stu1 = new Student();
		stu1.setNumber(123);
		stu1.setAddr(addr);
		
		Student stu2 = (Student)stu1.clone();
		
		System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
		System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
	}
}

打印结果:

  1. 学生1:123,地址:杭州市
  2. 学生2:123,地址:杭州市
学生1:123,地址:杭州市
学生2:123,地址:杭州市

乍一看没什么问题,真的是这样吗?

我们在main方法中试着改变addr实例的地址。

  1. addr.setAdd("西湖区");
  2. System.out.println("学生1:" + stu1.getNumber() +",地址:" + stu1.getAddr().getAdd());
  3. System.out.println("学生2:" + stu2.getNumber() +",地址:" + stu2.getAddr().getAdd());
		addr.setAdd("西湖区");
		
		System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
		System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());

打印结果:

  1. 学生1:123,地址:杭州市
  2. 学生2:123,地址:杭州市
  3. 学生1:123,地址:西湖区
  4. 学生2:123,地址:西湖区
学生1:123,地址:杭州市
学生2:123,地址:杭州市
学生1:123,地址:西湖区
学生2:123,地址:西湖区

这就奇怪了,怎么两个学生的地址都改变了?

原因是浅复制只是复制了addr变量的引用,并没有真正的开辟另一块空间,将值复制后再将引用返回给新对象。

所以,为了达到真正的复制对象,而不是纯粹引用复制。我们需要将Address类可复制化,并且修改clone方法,完整代码如下:

  1. package abc;
  2. class Addressimplements Cloneable {
  3. private String add;
  4. public String getAdd() {
  5. return add;
  6. }
  7. publicvoid setAdd(String add) {
  8. this.add = add;
  9. }
  10. @Override
  11. public Object clone() {
  12. Address addr =null;
  13. try{
  14. addr = (Address)super.clone();
  15. }catch(CloneNotSupportedException e) {
  16. e.printStackTrace();
  17. }
  18. return addr;
  19. }
  20. }
  21. class Studentimplements Cloneable{
  22. privateint number;
  23. private Address addr;
  24. public Address getAddr() {
  25. return addr;
  26. }
  27. publicvoid setAddr(Address addr) {
  28. this.addr = addr;
  29. }
  30. publicint getNumber() {
  31. return number;
  32. }
  33. publicvoid setNumber(int number) {
  34. this.number = number;
  35. }
  36. @Override
  37. public Object clone() {
  38. Student stu =null;
  39. try{
  40. stu = (Student)super.clone();//浅复制
  41. }catch(CloneNotSupportedException e) {
  42. e.printStackTrace();
  43. }
  44. stu.addr = (Address)addr.clone();//深度复制
  45. return stu;
  46. }
  47. }
  48. publicclass Test {
  49. publicstaticvoid main(String args[]) {
  50. Address addr =new Address();
  51. addr.setAdd("杭州市");
  52. Student stu1 =new Student();
  53. stu1.setNumber(123);
  54. stu1.setAddr(addr);
  55. Student stu2 = (Student)stu1.clone();
  56. System.out.println("学生1:" + stu1.getNumber() +",地址:" + stu1.getAddr().getAdd());
  57. System.out.println("学生2:" + stu2.getNumber() +",地址:" + stu2.getAddr().getAdd());
  58. addr.setAdd("西湖区");
  59. System.out.println("学生1:" + stu1.getNumber() +",地址:" + stu1.getAddr().getAdd());
  60. System.out.println("学生2:" + stu2.getNumber() +",地址:" + stu2.getAddr().getAdd());
  61. }
  62. }
package abc;

class Address implements Cloneable {
	private String add;

	public String getAdd() {
		return add;
	}

	public void setAdd(String add) {
		this.add = add;
	}
	
	@Override
	public Object clone() {
		Address addr = null;
		try{
			addr = (Address)super.clone();
		}catch(CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return addr;
	}
}

class Student implements Cloneable{
	private int number;

	private Address addr;
	
	public Address getAddr() {
		return addr;
	}

	public void setAddr(Address addr) {
		this.addr = addr;
	}

	public int getNumber() {
		return number;
	}

	public void setNumber(int number) {
		this.number = number;
	}
	
	@Override
	public Object clone() {
		Student stu = null;
		try{
			stu = (Student)super.clone();	//浅复制
		}catch(CloneNotSupportedException e) {
			e.printStackTrace();
		}
		stu.addr = (Address)addr.clone();	//深度复制
		return stu;
	}
}
public class Test {
	
	public static void main(String args[]) {
		
		Address addr = new Address();
		addr.setAdd("杭州市");
		Student stu1 = new Student();
		stu1.setNumber(123);
		stu1.setAddr(addr);
		
		Student stu2 = (Student)stu1.clone();
		
		System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
		System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
		
		addr.setAdd("西湖区");
		
		System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
		System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
	}
}

打印结果:

  1. 学生1:123,地址:杭州市
  2. 学生2:123,地址:杭州市
  3. 学生1:123,地址:西湖区
  4. 学生2:123,地址:杭州市
学生1:123,地址:杭州市
学生2:123,地址:杭州市
学生1:123,地址:西湖区
学生2:123,地址:杭州市

这样结果就符合我们的想法了。

总结:浅拷贝是指在拷贝对象时,对于基本数据类型的变量会重新复制一份,而对于引用类型的变量只是对引用进行拷贝,

没有对引用指向的对象进行拷贝。

而深拷贝是指在拷贝对象时,同时会对引用指向的对象进行拷贝。

区别就在于是否对 对象中的引用变量所指向的对象进行拷贝。

最后我们可以看看API里其中一个实现了clone方法的类:

java.util.Date:

  1. /**
  2. * Return a copy of this object.
  3. */
  4. public Object clone() {
  5. Date d =null;
  6. try {
  7. d = (Date)super.clone();
  8. if (cdate !=null) {
  9. d.cdate = (BaseCalendar.Date) cdate.clone();
  10. }
  11. }catch (CloneNotSupportedException e) {}// Won't happen
  12. return d;
  13. }
    /**
     * Return a copy of this object.
     */
    public Object clone() {
        Date d = null;
        try {
            d = (Date)super.clone();
            if (cdate != null) {
                d.cdate = (BaseCalendar.Date) cdate.clone();
            }
        } catch (CloneNotSupportedException e) {} // Won't happen
        return d;
    }






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值