引语
在 “值传递和引用传递” 这篇文章中我们提到了直接使用引用传递在方法中修改所指向对象的内容时实际是在修改实参,当我们想要复制对象进行修改而不影响原始对象时,我们就应根据实际需求选择浅拷贝或深拷贝。
深拷贝是指不仅复制对象本身,还递归复制对象中的所有引用类型成员变量所指向的对象,从而创建一个全新的、独立的对象。这样可以确保原始对象和克隆对象完全独立,对其中一个对象的修改不会影响到另一个对象。
概念
理解拷贝的概念前我们先做一个判断:
1 2 3
| MyClass a = new MyClass(); MyClass b; b = a;
|
这种方式中b
是不是a
的拷贝?
答案:否,b
其实是指向a
的一个引用,两个都对应内存中的同一个位置,因此修改b
的值也会改动a
的值。
在Java中拷贝对象(也称为克隆对象)有两种方式:浅拷贝和深拷贝。
拷贝要求拷贝出来的对象与原对象是独立的,浅拷贝和深拷贝与原对象都是独立的,区别仅在于:
- 浅拷贝:只复制对象本身和基本数据类型成员变量,而不复制对象中引用类型的成员变量所指向的对象。
- 深拷贝:不仅复制对象本身和基本数据类型成员变量,还递归复制对象中引用类型的成员变量所指向的对象。
特点
- 浅拷贝:只复制对象本身和基本数据类型成员变量,不复制对象中引用类型成员变量所指向的对象,修改克隆对象的基本数据类型成员变量不改变原对象,而克隆对象和原对象的引用类型成员变量所指向的对象是相同的。
- 深拷贝:不仅复制对象本身和基本数据类型成员变量,还递归复制对象中引用类型的成员变量所指向的对象,从而产生一个全新的独立于原对象的新对象。在新对象上做的所有修改都与原对象独立。
浅拷贝与深拷贝的主要区别只在对引用数据类型成员变量的处理上。
形象一点理解就是:
- 浅拷贝只复制对象的当前层,引用类型成员变量所指向的对象就属于更深一层的内容了,因此在浅拷贝中不复制此内容
- 深拷贝中会用递归的方式将所指向的对象也进行复制,从而产生完全一样完全独立的新对象。
浅拷贝
要完成浅拷贝,需要实现下面两个要求:
- 让自定义类实现
Cloneable
接口。
- 重写
.clone()
方法,返回值为Object
类型。
.clone()
方法捕获或抛出CloneNotSupportedException
异常
下面是使用例子:
- 示例1:在
.clone()
方法中捕获处理异常。浅拷贝示例11 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Class1 implements Cloneable{ private Integer data; private Class2 next; @Override public Object clone() { Class1 cloned = null; try { cloned = (Class1)super.clone(); }catch(CloneNotSupportedException e) { e.printStackTrace(); } return cloned; } }
|
- 示例2:在
.clone()
方法抛出异常,在使用处捕获处理异常。浅拷贝示例21 2 3 4 5 6 7 8 9 10 11
| class Class1 implements Cloneable throws CloneNotSupportedException{ private Integer data; private Class2 next; @Override public Object clone() { return (Class1)super.clone(); } }
|
深拷贝
深拷贝相比浅拷贝多了一个手动完成引用类型成员变量的克隆操作。
如果想给对象提供浅拷贝和深拷贝两种方式,就需要单独书写一个深拷贝方法,示例:
深拷贝示例11 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class Class1 implements Cloneable{ private Integer data; private Class2 next; @Override public Object clone() { Class1 cloned = null; try { cloned = (Class1) super.clone(); }catch(CloneNotSupportedException e) { e.printStackTrace(); } return cloned; }
public Class1 deepClone() { Class1 cloned = null; try { cloned = (Class1)super.clone(); cloned.next = (Class2)next.clone(); }catch(CloneNotSupportedException e) { e.printStackTrace(); } return cloned; } }
|
有时候我们针对一个对象只需要深拷贝这一种拷贝方法,那么就可以直接在.clone()方法中实现深拷贝,示例:
深拷贝示例21 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Class1 implements Cloneable{ private Integer data; private Class2 next; @Override public Object clone() { Class1 cloned = null; try { cloned = (Class1)super.clone(); cloned.next = (Class2)next.clone(); }catch(CloneNotSupportedException e) { e.printStackTrace(); } return cloned; } }
|
两种深拷贝方式都是可以的,根据场景选择适合的。上面介绍的深拷贝都是通过克隆实现的,即实现Cloneable
接口,还有一种方式是通过序列化和反序列化来完成深拷贝,需要实现``接口
注意:要实现深拷贝,引用类型成员变量的这个类(例如前文代码中的Class2)也要实现Cloneable
接口,并重写.clone()
方法。
序列化
有的场景下类中含有多个引用类型,或者引用类型内层还包含很多引用类型,那这种场景下如果还是用Cloneable
接口的.clone()
方式来书写就会特别麻烦,这时候可以考虑使用序列化来完成深度拷贝。
序列化(Serialization)就是将对象写到流的过程,写入完成后流中的对象是原有对象的一份拷贝,原对象仍然存在内存中,两者相互独立。对象序列化时不仅复制对象本身,还递归复制其引用的成员对象,因此通过序列化和反序列化过程就可以实现深拷贝。要完成深拷贝有下面的要求:
- 要实现序列化的对象类必须实现
Serializable
接口。
- 要实现序列化的对象中要复制的成员变量的对象类必须实现
Serializable
接口。
- 在深拷贝方法中包含对象的序列化和反序列化
具体实现示例:
序列化深拷贝1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| class Class4 implements Serializable{ private String name; }
class Class3 implements Serializable{ private Integer data; private Class4 next; public Class3 deepClone() { Class3 cloned = 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); cloned = (Class3)ois.readObject(); }catch(IOException e) { e.printStackTrace(); }catch(ClassNotFoundException e) { e.printStackTrace(); } return cloned; } }
|
采用序列化实现的深拷贝与实现Cloneable
接口的深拷贝有什么区别?
通过序列化和反序列化实现的深拷贝相比另一种深拷贝,多了“泛型限定”这一优势,编译器会在运行前检查待克隆的对象是否支持序列化,从而避免在运行时抛出异常。
tips: 当你想要让类中的某个成员变量不被深拷贝,也就是说不被序列化和反序列化,那就用transient
关键字修饰该成员变量,或者将此成员变量加static
修饰变为静态变量,因为静态变量归属于类,不会进入对象的序列化和反序列化过程。
总结
- 浅拷贝和深拷贝都可通过实现
Cloneable
接口,并重写.clone()
来定义,两者区别在于深拷贝的引用成员变量的复制要手动完成。
- 深拷贝还可通过对象的序列化和反序列化来实现,相应的需要被拷贝的对象实现
Serializable
接口。
- 通过对象的序列化和反序列化实现的深拷贝能更好处理“引用嵌套,多个引用”这种情况,同时具备“泛型限定”的优势,在实现深拷贝时应优先采用这种方式。
附录
贴上完整测试代码:
完整测试代码1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
| import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable;
class Class2 implements Cloneable{ private String name; public Object clone() { Class2 cloned = null; try { cloned = (Class2)super.clone(); }catch(CloneNotSupportedException e) { e.printStackTrace(); } return cloned; } public void setName(String str) { name = str; } public String getName() { return name; } }
class Class1 implements Cloneable{ private Integer data; private Class2 next; @Override public Object clone() { Class1 cloned = null; try { cloned = (Class1) super.clone(); }catch(CloneNotSupportedException e) { e.printStackTrace(); } return cloned; } public void setData(Integer value) { this.data = value; } public void setNext(Class2 myclass) { this.next = myclass; } public Class2 getNext() { return (Class2) this.next; } public void display() { System.out.println("data = " + data); System.out.println("next address = " + next); System.out.println("next value = " + next.getName()); } public Class1 deepClone() { Class1 cloned = null; try { cloned = (Class1)super.clone(); cloned.next = (Class2)next.clone(); }catch(CloneNotSupportedException e) { e.printStackTrace(); } return cloned; } }
class Class4 implements Serializable{ private String name; public void setName(String str) { name = str; } public String getName() { return name; } }
class Class3 implements Serializable{ private Integer data; private Class4 next; public Class3 deepClone() { Class3 cloned = 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); cloned = (Class3)ois.readObject(); }catch(IOException e) { e.printStackTrace(); }catch(ClassNotFoundException e) { e.printStackTrace(); } return cloned; } public void setData(Integer value) { this.data = value; } public void setNext(Class4 myclass) { this.next = myclass; } public Class4 getNext() { return (Class4) this.next; } public void display() { System.out.println("data = " + data); System.out.println("next address = " + next); System.out.println("next value = " + next.getName()); } }
public class CopyMethod { public static void test1(Class1 origin, Class1 cloned) { System.out.println("Origin: "); origin.display(); System.out.println("Cloned: "); cloned.display(); System.out.println("--- Cloneable after change --- "); cloned.setData(2); cloned.getNext().setName("XiaoMing"); System.out.println("Origin: "); origin.display(); System.out.println("Cloned: "); cloned.display(); } public static void test2(Class3 origin, Class3 cloned) { System.out.println("Origin: "); origin.display(); System.out.println("Cloned: "); cloned.display(); System.out.println("--- after change --- "); cloned.setData(2); cloned.getNext().setName("XiaoMing"); System.out.println("Origin: "); origin.display(); System.out.println("Cloned: "); cloned.display(); } public static void main(String[] args) { Class1 origin = new Class1(); origin.setData(1); Class2 next = new Class2(); next.setName("LiHua"); origin.setNext(next); Class1 shallowCloned = (Class1) origin.clone(); System.out.println("\n===== Shallow copy Test =====\n"); test1(origin, shallowCloned); origin.getNext().setName("LiHua"); Class1 deepCloned = (Class1) origin.deepClone(); System.out.println("\n=====Cloneable Deep copy Test =====\n"); test1(origin, deepCloned); Class3 origin2 = new Class3(); origin2.setData(1); Class4 next2 = new Class4(); next2.setName("LiHua"); origin2.setNext(next2); Class3 deepCloned2 = (Class3)origin2.deepClone(); System.out.println("\n=====Serializable Deep copy Test =====\n"); test2(origin2, deepCloned2); } }
|