简介
对象拷贝有哪些?
对象拷贝(Object Copy)就是将一个对象的属性拷贝到另一个有着相同类类型的对象中去。在程序中拷贝对象是很常见的,主要是为了在新的上下文环境中复用对象的部分或全部数据。
Java中主要有三种类型的对象拷贝:
- 浅拷贝(Shallow Copy)
- 深拷贝(Deep Copy)
- 延迟拷贝(Lazy Copy)
当然有一种是引用拷贝
引用拷贝
引用拷贝会生成一个新的对象引用地址,但是两个最终指向依然是同一个对象。如何更好的理解引用拷贝呢?很简单,就拿我们人来说,通常有个姓名,但是不同场合、人物对我们的叫法可能不同,但我们很清楚哪些名称都是属于”我”的!

当然,通过一个代码示例让大家领略一下(为了简便就不写get、set等方法):
class Son { String name; int age;
public Son(String name, int age) { this.name = name; this.age = age; } } public class test { public static void main(String[] args) { Son s1 = new Son("son1", 12); Son s2 = s1; s1.age = 22; System.out.println(s1); System.out.println(s2); System.out.println("s1的age:" + s1.age); System.out.println("s2的age:" + s2.age); System.out.println("s1==s2" + (s1 == s2)); } }
|
输出的结果为:
Son@135fbaa4 Son@135fbaa4 s1的age:22 s2的age:22 true
|
创建对象的5种方式
- 通过 new 关键字
这是最常用的一种方式,通过 new 关键字调用类的有参或无参构造方法来创建对象。比如 Object obj = new Object();
- 通过 Class 类的 newInstance() 方法
这种默认是调用类的无参构造方法创建对象。比如 Person p2 = (Person) Class.forName("com.ys.test.Person").newInstance();
- 通过 Constructor 类的 newInstance 方法
这和第二种方法类时,都是通过反射来实现。通过 java.lang.relect.Constructor 类的 newInstance() 方法指定某个构造器来创建对象。
Person p3 = (Person) Person.class.getConstructors()[0].newInstance();
实际上第二种方法利用 Class 的 newInstance() 方法创建对象,其内部调用还是 Constructor 的 newInstance() 方法。
- 利用 Clone 方法
Clone 是 Object 类中的一个方法,通过 对象A.clone() 方法会创建一个内容和对象 A 一模一样的对象 B,clone 克隆,顾名思义就是创建一个一模一样的对象出来。
Person p4 = (Person) p3.clone();
- 反序列化
序列化是把堆内存中的 Java 对象数据,通过某种方式把对象存储到磁盘文件中或者传递给其他网络节点(在网络上传输)。而反序列化则是把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。
浅拷贝
简介
先介绍一点铺垫知识:Java中的数据类型分为基本数据类型和引用数据类型。对于这两种数据类型,在进行赋值操作、用作方法参数或返回值时,会有值传递和引用(地址)传递的差别。
对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。
对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。

具体模型如图所示:可以看到基本数据类型的成员变量,对其值创建了新的拷贝。而引用数据类型的成员变量的实例仍然是只有一份,两个对象的该成员变量都指向同一个实例。
如果用一张图来描述一下浅拷贝,它应该是这样的:

浅拷贝的实现
通过重写clone()方法进行浅拷贝:
Object类是类结构的根类,其中有一个方法为protected Object clone() throws CloneNotSupportedException,这个方法就是进行的浅拷贝。有了这个浅拷贝模板,我们可以通过调用clone()方法来实现对象的浅拷贝。
但是需要注意:
参考代码如下:对Student类的对象进行拷贝,直接重写clone()方法,通过调用clone方法即可完成浅拷贝。
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; }
@Override public String toString() { return "[Subject: " + this.hashCode() + ",name:" + name + "]"; } }
|
public class Student implements Cloneable {
private Subject subject; private String name; private int age;
public Subject getSubject() { return subject; }
public void setSubject(Subject subject) { this.subject = subject; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { return null; } }
@Override public String toString() { return "[Student: " + this.hashCode() + ",subject:" + subject + ",name:" + name + ",age:" + age + "]"; } }
|
public class ShallowCopy { public static void main(String[] args) { Subject subject = new Subject("yuwen"); Student studentA = new Student(); studentA.setSubject(subject); studentA.setName("Lynn"); studentA.setAge(20); Student studentB = (Student) studentA.clone(); studentB.setName("Lily"); studentB.setAge(18); Subject subjectB = studentB.getSubject(); subjectB.setName("lishi"); System.out.println("studentA:" + studentA.toString()); System.out.println("studentB:" + studentB.toString()); } }
|
输出的结果:
studentA:[Student: 460141958,subject:[Subject: 1163157884,name:lishi],name:Lynn,age:20] studentB:[Student: 1956725890,subject:[Subject: 1163157884,name:lishi],name:Lily,age:18]
|
由输出的结果可见,通过 studentA.clone()
拷贝对象后得到的 studentB
,和 studentA
是两个不同的对象。studentA
和 studentB
的基础数据类型的修改互不影响,而引用类型 subject
修改后是会有影响的。
浅拷贝和对象拷贝的区别
public static void main(String[] args) { Subject subject = new Subject("yuwen"); Student studentA = new Student(); studentA.setSubject(subject); studentA.setName("Lynn"); studentA.setAge(20); Student studentB = studentA; studentB.setName("Lily"); studentB.setAge(18); Subject subjectB = studentB.getSubject(); subjectB.setName("lishi"); System.out.println("studentA:" + studentA.toString()); System.out.println("studentB:" + studentB.toString()); }
|
这里把 Student studentB = (Student) studentA.clone()
换成了 Student studentB = studentA
。
输出的结果:
studentA:[Student: 460141958,subject:[Subject: 1163157884,name:lishi],name:Lily,age:18] studentB:[Student: 460141958,subject:[Subject: 1163157884,name:lishi],name:Lily,age:18]
|
可见,对象拷贝后没有生成新的对象,二者的对象地址是一样的;而浅拷贝的对象地址是不一样的。
深拷贝
简介
通过上面的例子可以看到,浅拷贝会带来数据安全方面的隐患,例如我们只是想修改了 studentB
的 subject
,但是 studentA
的 subject
也被修改了,因为它们都是指向的同一个地址。所以,此种情况下,我们需要用到深拷贝。
深拷贝,在拷贝引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。
深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
简单地说,深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间。
深拷贝模型如图所示:可以看到所有的成员变量都进行了复制。

在上图中,SourceObject有一个int类型的属性 “field1”和一个引用类型属性”refObj1”(引用ContainedObject类型的对象)。当对SourceObject做深拷贝时,创建了CopiedObject,它有一个包含”field1”拷贝值的属性”field2”以及包含”refObj1”拷贝值的引用类型属性”refObj2” 。因此对SourceObject中的”refObj”所做的任何改变都不会影响到CopiedObject
深拷贝的特点
- 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个(和浅拷贝一样)。
- 对于引用类型,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。改变其中一个,不会对另外一个也产生影响。
- 对于有多层对象的,每个对象都需要实现
Cloneable
并重写 clone()
方法,进而实现了对象的串行层层拷贝。 - 深拷贝相比于浅拷贝速度较慢并且花销较大。
深拷贝的实现
通过重写clone方法来实现深拷贝
与通过重写clone方法实现浅拷贝的基本思路一样,只需要为对象图的每一层的每一个对象都实现Cloneable接口并重写clone方法,最后在最顶层的类的重写的clone方法中调用所有的clone方法即可实现深拷贝。简单的说就是:每一层的每个对象都进行浅拷贝=深拷贝。
参考代码如下:
对于 Student
的引用类型的成员变量 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 { return super.clone(); }
@Override public String toString() { return "[Subject: " + this.hashCode() + ",name:" + name + "]"; } }
|
在 Student
的 clone()
方法中,需要拿到拷贝自己后产生的新的对象,然后对新的对象的引用类型再调用拷贝操作,实现对引用类型成员变量的深拷贝。
public class Student implements Cloneable {
private Subject subject; private String name; private int age;
public Subject getSubject() { return subject; }
public void setSubject(Subject subject) { this.subject = subject; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override public Object clone() { try { Student student = (Student) super.clone(); student.subject = (Subject) subject.clone(); return student; } catch (CloneNotSupportedException e) { return null; } }
@Override public String toString() { return "[Student: " + this.hashCode() + ",subject:" + subject + ",name:" + name + ",age:" + age + "]"; } }
|
一样的使用方式
public class DeepCopy { public static void main(String[] args) { Subject subject = new Subject("yuwen"); Student studentA = new Student(); studentA.setSubject(subject); studentA.setName("Lynn"); studentA.setAge(20); Student studentB = (Student) studentA.clone(); studentB.setName("Lily"); studentB.setAge(18); Subject subjectB = studentB.getSubject(); subjectB.setName("lishi"); System.out.println("studentA:" + studentA.toString()); System.out.println("studentB:" + studentB.toString()); } }
|
输出结果:
studentA:[Student: 460141958,subject:[Subject: 1163157884,name:yuwen],name:Lynn,age:20] studentB:[Student: 1956725890,subject:[Subject: 356573597,name:lishi],name:Lily,age:18]
|
由输出结果可见,深拷贝后,不管是基础数据类型还是引用类型的成员变量,修改其值都不会相互造成影响。
通过对象序列化实现深拷贝
参考代码如下:
对于 Student
的引用类型的成员变量 Subject
,需要实现 Serializable
接口。
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 public String toString() { return "[Subject: " + this.hashCode() + ",name:" + name + "]"; } }
|
对于 Student
类同样要实现Serializable
接口
public class Student implements Serializable {
private Subject subject; private String name; private int age;
public Subject getSubject() { return subject; }
public void setSubject(Subject subject) { this.subject = subject; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override public String toString() { return "[Student: " + this.hashCode() + ",subject:" + subject + ",name:" + name + ",age:" + age + "]"; } }
|
需要序列化再进行深拷贝
public class DeepCopy { public static void main(String[] args) { Subject subject = new Subject("yuwen"); Student studentA = new Student(); studentA.setSubject(subject); studentA.setName("Lynn"); studentA.setAge(20); ByteArrayOutputStream bos=new ByteArrayOutputStream(); ObjectOutputStream oos=new ObjectOutputStream(bos); oos.writeObject(studentA); oos.flush(); ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); Student studentB=(Student)ois.readObject(); studentB.setName("Lily"); studentB.setAge(18); Subject subjectB = studentB.getSubject(); subjectB.setName("lishi"); System.out.println("studentA:" + studentA.toString()); System.out.println("studentB:" + studentB.toString()); } }
|
输出结果:
studentA:[Student: 460141958,subject:[Subject: 1163157884,name:yuwen],name:Lynn,age:20] studentB:[Student: 1956725890,subject:[Subject: 356573597,name:lishi],name:Lily,age:18]
|
延迟拷贝
- 延迟拷贝是浅拷贝和深拷贝的一个组合,实际上很少会使用。这个以前几乎都没听说过,后来看书才知道有这么一种拷贝!
- 当最开始拷贝一个对象时,会使用速度较快的浅拷贝,还会使用一个计数器来记录有多少对象共享这个数据。当程序想要修改原始的对象时,它会决定数据是否被共享(通过检查计数器)并根据需要进行深拷贝。
- 延迟拷贝从外面看起来就是深拷贝,但是只要有可能它就会利用浅拷贝的速度。当原始对象中的引用不经常改变的时候可以使用延迟拷贝。由于存在计数器,效率下降很高,但只是常量级的开销。而且, 在某些情况下, 循环引用会导致一些问题。
如何选择拷贝方式
- 如果对象的属性全是基本类型的,那么可以使用浅拷贝。
- 如果对象有引用属性,那就要基于具体的需求来选择浅拷贝还是深拷贝。
- 意思是如果对象引用任何时候都不会被改变,那么没必要使用深拷贝,只需要使用浅拷贝就行了。如果对象引用经常改变,那么就要使用深拷贝。没有一成不变的规则,一切都取决于具体需求。
数组的拷贝
数组除了默认实现了clone()方法之外,还提供了Arrays.copyOf方法用于拷贝,这两者都是浅拷贝。
浅拷贝
在堆内存中不会分配新的空间,而是增加一个引用变量和之前的引用指向相同的堆空间。
详释:
- 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。
- 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
参考代码如下:
public class Test { public static void main(String[] args) { int[] a = {1,2,3,4,5}; int[] b = a; for (int i = 0; i < b.length; i++) { System.out.print(b[i] + " "); } System.out.println(); } }
|
运行结果为
深拷贝
在堆内存中分配新空间,将之前的数组堆内存中的内容拷贝到新的空间中。
详释:
对于深拷贝来说,不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷贝。
System.arraycopy() 方法实现数组拷贝
- 先来看看基本数据类型的System.arraycopy() 方法拷贝:
public class TestDemo { public static void main(String[] args) { int[] array1 = new int[]{1,2,8,7,6}; int[] array2 = new int[array1.length]; System.arraycopy(array1, 0, array2, 0, array1.length); System.out.println("array1 = " + Arrays.toString(array1)); System.out.println("array2 = " + Arrays.toString(array2)); System.out.println("========================="); array2[0] = 100; System.out.println("array1 = " + Arrays.toString(array1)); System.out.println("array2 = " + Arrays.toString(array2)); } }
|
这段程序的结果是:
array1 = [1, 2, 8, 7, 6] array2 = [1, 2, 8, 7, 6] ========================= array1 = [1, 2, 8, 7, 6] array2 = [100, 2, 8, 7, 6]
|
由结果可以看出,当对复制数组的某个元素进行改变时,并不影响被复制数组对应元素,即对于基本数据类型来说System.arraycopy() 方法是深拷贝。
看一下内存分析:

- 引用数据类型时的System.arraycopy() 方法拷贝
class TestArray{ private int val = 10; public void setVal(int val){ this.val = val; } public int getVal(){ return this.val; } } public class TestDemo { public static void printArray(TestArray[] array){ for(int i = 0;i < array.length;i++){ System.out.print(array[i].getVal()+" "); } System.out.println(); } public static void main(String[] args) { TestArray[] array1 = new TestArray[3]; for (int i = 0; i < array1.length; i++){ array1[i] = new TestArray(); } TestArray[] array2 = new TestArray[array1.length]; System.arraycopy(array1,0,array2,0,array1.length); printArray(array1); printArray(array2); System.out.println("=========="); array2[0].setVal(100);; printArray(array1); printArray(array2); } }
|
这段程序的结果是:
10 10 10 10 10 10 ========== 100 10 10 100 10 10
|
由结果可以看出,当对复制数组的某个元素进行改变时,被复制数组对应元素也随之改变,即对于引用数据类型来说 System.arraycopy() 方法是浅拷贝。
看一下内存分析:

通过内存分析可以很直观的看到,每个数组的元素分别指向同一个内存地址,当通过其中一个数组的某个元素对被指向地址的数值进行更改时,另一个数组相应的元素同样会发生改变。
Arrays.copyOf()方法实现数组拷贝
先来看看Arrays.copyOf()方法的源码:
public static int[] copyOf(int[] original, int newLength) { int[] copy = new int[newLength]; System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }
|
可以看到其实Arrays.copyOf()方法在底层是调用了 System.arraycopy() 方法来实现复制,即可以把Arrays.copyOf() 方法看作是 System.arraycopy() 方法的衍生方法,故它的执行机理与 System.arraycopy() 方法相同。
所以 Arrays.copyOf() 方法对于基本数据类型来说是深拷贝,对引用类型来说是浅拷贝。
String类型非常特殊,首先,String类型属于引用数据类型,不属于基本数据类型,但是String类型的数据是存放在常量池中的,也就是无法修改的!
List的拷贝
集合的拷贝也是我们平时经常会遇到的,一般情况下,我们都是用浅拷贝来实现,即通过构造函数或者clone方法。
List浅拷贝

众所周知,list本质上是数组,而数组的是以地址的形式进行存储。
如上图将list A浅拷贝给list B,由于进行的是浅拷贝,所以直接将A的内容复制给了B,java中相同内容的数组指向同一地址,即进行浅拷贝后A与B指向同一地址。造成的后果就是,改变B的同时也会改变A,因为改变B就是改变B所指向地址的内容,由于A也指向同一地址,所以A与B一起改变。
遍历循环复制
List<Person> destList=new ArrayList<Person>(srcList.size()); for(Person p : srcList){ destList.add(p); }
|
使用List实现类的构造方法
List<Person> destList=new ArrayList<Person>(srcList);
|
使用list.addAll()方法
List<Person> destList=new ArrayList<Person>(); destList.addAll(srcList);
|
使用System.arraycopy()方法
Person[] srcPersons = srcList.toArray(new Person[0]); Person[] destPersons = new Person[srcPersons.length]; System.arraycopy(srcPersons, 0, destPersons, 0, srcPersons.length);
|
使用List的clone()方法
ArrayList destList = (ArrayList) srcList.clone();
|
测试及结果
printList(destList); //打印未改变B之前的A srcList.get(0).setAge(100);//改变B System.out.println("=========="); printList(destList); //打印改变B后的A
//打印结果 123-->20 ABC-->21 abc-->22 ========== 123-->100 ABC-->21 abc-->22
|
List深拷贝

如图,深拷贝就是将A复制给B的同时,给B创建新的地址,再将地址A的内容传递到地址B。ListA与ListB内容一致,但是由于所指向的地址不同,所以改变相互不受影响。
深拷贝的实现
- 使用序列化方法
public static <T> List<T> deepCopy(List<T> src) throws IOException, ClassNotFoundException { ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(byteOut); out.writeObject(src); ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray()); ObjectInputStream in = new ObjectInputStream(byteIn); @SuppressWarnings("unchecked") List<T> dest = (List<T>) in.readObject(); return dest; } List<Person> destList=deepCopy(srcList);
|
- clone方法
public class A implements Cloneable { public String name[]; public A(){ name=new String[2]; } public Object clone() { A o = null; try { o = (A) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return o; } } for(int i=0;i<n;i+=){ copy.add((A)src.get(i).clone()); }
|
Java对对象和基本的数据类型的处理是不一样的。在Java中用对象的作为入口参数的传递则缺省为”引用传递”,也就是说仅仅传递了对象的一个”引用”,这个”引用”的概念同C语言中的指针引用是一样的。当函数体内部对输入变量改变时,实质上就是在对这个对象的直接操作。
除了在函数传值的时候是”引用传递”,在任何用”=”向对象变量赋值的时候都是”引用传递”。
测试及结果
printList(destList); //打印未改变B之前的A srcList.get(0).setAge(100);//改变B System.out.println("=========="); printList(destList); //打印改变B后的A
123-->20 ABC-->21 abc-->22 ========== 123-->20 ABC-->21 abc-->22
|
在浅复制的情况下,源数据被修改破坏之后,使用相同引用指向该数据的目标集合中的对应元素也就发生了相同的变化。因此,在需求要求必须深复制的情况下,要是使用上面提到的方法,请确保List中的T类对象是不易被外部修改和破坏的。
参考文章:
(9条消息) Java数组复制(浅/深拷贝)之二_AlbenXie的博客-CSDN博客
Java 浅拷贝和深拷贝 - 云+社区 - 腾讯云 (tencent.com)
Java深拷贝和浅拷贝 - 云+社区 - 腾讯云 (tencent.com)
ArrayList浅、深拷贝 - 云+社区 - 腾讯云 (tencent.com)
Java List复制:浅拷贝与深拷贝方法及区别 (qq.com)