一、引言
物件拷貝(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 Object: John - Algebrainal Cloned Object: John - Algebram
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 Physupated: true
Original Object after it is Physupated Physics PhysicsCup ics
在這個例子中,我讓要拷貝的類別Student實作了Clonable介面並重寫Object類別的clone()方法,然後在方法內部呼叫super.clone()方法。從輸出結果中我們可以看到,對原始物件stud的"name"屬性所做的改變並沒有影響到拷貝物件clonedStud,但是對引用物件subj的"name"屬性所做的改變影響到了拷貝物件clonedStud。
1、什麼是深拷貝
深拷貝會拷貝所有的屬性,並拷貝屬性指向的動態分配的記憶體。當物件和它所引用的物件一起拷貝時即發生深拷貝。深拷貝相比於淺拷貝速度較慢且花銷較大。
下面是實現深拷貝的一個例子。只是在淺拷貝的例子上做了一點小改動,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
因為它是深拷貝,所以你需要建立拷貝類別的一個物件。因為在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倍的时间。
四、延迟拷贝
延迟拷贝是浅拷贝和深拷贝的一个组合,实际上很少会使用。 当最开始拷贝一个对象时,会使用速度较快的浅拷贝,还会使用一个计数器来记录有多少对象共享这个数据。当程序想要修改原始的对象时,它会决定数据是否被共享(通过检查计数器)并根据需要进行深拷贝。
延迟拷贝从外面看起来就是深拷贝,但是只要有可能它就会利用浅拷贝的速度。当原始对象中的引用不经常改变的时候可以使用延迟拷贝。由于存在计数器,效率下降很高,但只是常量级的开销。而且, 在某些情况下, 循环引用会导致一些问题。
五、如何选择
如果对象的属性全是基本类型的,那么可以使用浅拷贝,但是如果对象有引用属性,那就要基于具体的需求来选择浅拷贝还是深拷贝。我的意思是如果对象引用任何时候都不会被改变,那么没必要使用深拷贝,只需要使用浅拷贝就行了。如果对象引用经常改变,那么就要使用深拷贝。没有一成不变的规则,一切都取决于具体需求。
更多Java中的深拷貝與淺拷貝介紹相关文章请关注PHP中文网!