前段時間碰到需要將一個Java對象進行深度拷貝的情況,但是JDK並未提供關於deep copy相關的API,唯一能用的就是一個不太穩定的clone(),所以問題就來了,如何實現穩定的deep copy,下面就實現deep copy的方法做個介紹。
1. 直接賦值
實現deep copy,首先想到的是可以直接賦值麼?如下:
- Test test = new Test();
- Test test2 = test;
- System.out.println(test);
- System.out.println(test2);
上面的代碼裏,直接將test複製給test2,但是將兩個對象打印出來發現,地址其實是一樣的,test只是剛剛在堆上分配的Test對象的引用,而這裏的賦值直接是引用直接的賦值,等於test2也是指向剛剛new出來的對象,這裏的copy就是一個shallow copy,及只是copy了一份引用,但是對象實體並未copy,既然賦值不行,那就試試第二個方法,Object類的clone方法。
2. clone方法
1. clone方法介紹
Java中所有對象都繼承自Object類,所以就默認自帶clone方法的實現,clone方法的實現是比較簡單粗暴的。首先,如果一個對象想要調用clone方法,必須實現Cloneable接口,否則會拋出CloneNotSupportedException。其實這個Cloneable是個空接口,只是個flag用來標記這個類是可以clone的,所以說將一個類聲明爲Cloneable與這個類具備clone能力其實並不是直接相關的。其實Cloneable是想表明具有複製這種功能,所以按理說clone應該作爲Cloneable的一個方法而存在,但是實際上clone方法是Object類的一個protected方法,所以你無法直接通過多態的方式調用clone方法,比如:
- public class Test implements Cloneable {
- public static void main(String[] args) {
- try {
- List<Cloneable> list = new ArrayList<Cloneable>();
- Cloneable t1 = new InnerTest("test");
- list.add(t1);
- list.add(t1.clone()); // 事實上,我無法這麼做
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- public static class InnerTest implements Cloneable {
- public String a;
- public InnerTest(String test) {
- a = test;
- }
- public Object clone() throws CloneNotSupportedException {
- return super.clone();
- }
- }
- }
2. clone是深複製還是淺複製
當調用clone方法時,首先會直接分配內存,然後將原對象內所有的字段都一一複製,如果字段是基本類型數據比如int之類的,則這樣直接的賦值式的複製毫無問題,但是如果字段是引用的話問題就來了,引用也會原封不動的複製一份,就如同第一個例子一樣。所以,很多情景下,clone只能算一個半deep半shallow的複製方法。想要解決這個問題,唯一的方法就是在需要被複制的對象的clone方法內調用會被shallow copy的對象的clone方法,但是前提是該對象也繼承了Cloneable接口並Override了clone方法。比如:
- public class Test implements Cloneable {
- public static void main(String[] args) {
- try {
- InnerTest t1 = new InnerTest(new InnerTest2());
- InnerTest t2 = (InnerTest) t1.clone();
- System.out.println(t1); // Test$InnerTest@232204a1
- System.out.println(t2); // Test$InnerTest@4aa298b7
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- public static class InnerTest implements Cloneable {
- public InnerTest2 test;
- public InnerTest(InnerTest2 test) {
- this.test = test;
- }
- @Override
- public Object clone() throws CloneNotSupportedException {
- return super.clone();
- }
- }
- public static class InnerTest2 implements Cloneable {
- public InnerTest2() {
- }
- @Override
- public Object clone() throws CloneNotSupportedException {
- return super.clone();
- }
- }
- }
3. clone跳過構造函數
此外,clone方法不通過構造函數來創建新對象,所以構造函數內的邏輯也會被直接跳過,這也會帶來問題,等於clone引進了一個我們無法控制的對象構造方法。比如想在構造函數內實現一個計數功能,每次new就加1,但是如果clone的話,則這個計數就無法生效。比如:
- public class Test implements Cloneable {
- public static void main(String[] args) {
- try {
- List<Cloneable> list = new ArrayList<Cloneable>();
- InnerTest t1 = new InnerTest("test");
- InnerTest t2 = new InnerTest("test1");
- list.add(t1);
- list.add(t2);
- list.add((Cloneable) t1.clone());
- for (Cloneable c : list) {
- System.out.println(((InnerTest) c).index ); // 依次打印 0 1 0
- }
- System.out.println(InnerTest.count); // count爲2
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- public static class InnerTest implements Cloneable {
- public int index;
- public static int count = 0;
- public InnerTest(String test) {
- index = count;
- count++;
- }
- public Object clone() throws CloneNotSupportedException {
- return super.clone();
- }
- }
- }
4. 最佳實踐——複製構造函數或者自定義Copyable接口
另外clone方法本身也是線程不安全的。所以總結下來就是clone是很不靠譜的,所以主流的建議還是添加複製構造函數,這樣雖然會比較麻煩一點,但是可控性強且可以實現deep copy。此外也可以自己實現一套Copyable接口,然後想要複製的類都繼承該接口並復現copy函數即可。但是copy函數內的邏輯其實與複製構造類似。比如:
Copyable接口:
- public interface Copyable<T> {
- T copy ();
- }
具體實現與測試:
- public class Test{
- public static void main(String[] args) {
- try {
- InnerTest t1 = new InnerTest(new InnerTest2());
- InnerTest t2 = t1.copy();
- System.out.println(t1.test.getA()); // print 0
- t1.test.setA(5);
- System.out.println(t2.test.getA()); // print 0
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- // 測試類
- public static class InnerTest implements Copyable<InnerTest> {
- // set to public for convenience
- public InnerTest2 test;
- public InnerTest(InnerTest2 tmp) {
- this.test = tmp;
- }
- @Override
- public InnerTest copy() {
- InnerTest2 tmp = test == null ? null : test.copy();
- return new InnerTest(tmp);
- }
- }
- // 測試類,增加getter和setter方法來驗證
- public static class InnerTest2 implements Copyable<InnerTest2>{
- private int a;
- public InnerTest2() {
- a = 0;
- }
- public int getA() {
- return a;
- }
- public void setA(int a) {
- this.a = a;
- }
- @Override
- public InnerTest2 copy() {
- InnerTest2 tmp = new InnerTest2();
- tmp.setA(this.a);
- return tmp;
- }
- }
- }
3. 序列化實現深複製
1. 爲什麼使用序列化
其實大部分情況下複製構造是個不錯的選擇,但是實現上來說確實比較繁瑣,且容易出錯,因爲需要遞歸式的將所有的對象和它引用的對象都進行復制,所以就有了另外一種實現deep copy的思路:Java Object Serialization (JOS)。序列化會將一個對象的各個方面都考慮到,包括父類,各個字段,以及各種引用。所以如果將一個對象先序列化寫入字節流,然後再讀出,重新構造成一個對象,就能實現這個對象的deep copy。當然,這裏其實也沒考慮構造函數邏輯,但是這種方法卻不需要考慮會有shallow copy的可能,而且省去了繁瑣的複製構造或者copy方法的覆寫,我們可以直接通過一個實現一個deepCopy函數來實現對象複製。下面就對這種方法做一個介紹。
2. 深複製的實現
- public class Test2 {
- public static Object deepCopy(Object from) {
- Object obj = null;
- try {
- // 將對象寫成 Byte Array
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- ObjectOutputStream out = new ObjectOutputStream(bos);
- out.writeObject(from);
- out.flush();
- out.close();
- // 從流中讀出 byte array,調用readObject函數反序列化出對象
- ObjectInputStream in = new ObjectInputStream(
- new ByteArrayInputStream(bos.toByteArray()));
- obj = in.readObject();
- } catch(IOException e) {
- e.printStackTrace();
- } catch(ClassNotFoundException e2) {
- e2.printStackTrace();
- }
- return obj;
- }
- }
3.序列化存在的問題
4. 使用相關第三方庫
- 實現Cloneable接口並覆寫clone方法
- 使用複製構造函數
- 自定義一個Copyable接口,然後爲需要clone的類增加copy方法的具體實現
- 通過序列化方式將一個對象先序列化再反序列化得到一個deep copy的新對象
- 使用成熟第三方庫,具體方法看文檔。