在有些業務場景下,我們需要兩個完全相同卻彼此無關的java對象。比如使用原型模式、多線程編程等。對此,java提供了深拷貝的概念。通過深度拷貝可以從源對象完美複製出一個相同卻與源對象彼此獨立的目標對象。這裡的相同是指兩個對象的狀態和動作相同,彼此獨立是指改變其中一個對象的狀態不會影響到另外一個對象。 ...
在有些業務場景下,我們需要兩個完全相同卻彼此無關的java對象。比如使用原型模式、多線程編程等。對此,java提供了深拷貝的概念。通過深度拷貝可以從源對象完美複製出一個相同卻與源對象彼此獨立的目標對象。這裡的相同是指兩個對象的狀態和動作相同,彼此獨立是指改變其中一個對象的狀態不會影響到另外一個對象。實現深拷貝常用的實現方式有2種:Serializable,Cloneable。
Serializable方式就是通過java對象的序列化和反序列化的操作實現對象拷貝的一種比較常見的方式。本來java對象們都待在虛擬機堆中,通過序列化,將源對象的信息以另外一種形式存放在了堆外。這時源對象的信息就存在了2份,一份在堆內,一份在堆外。然後將堆外的這份信息通過反序列化的方式再放回到堆中,就創建了一個新的對象,也就是目標對象。
--Serializable代碼
public static Object cloneObjBySerialization(Serializable src) { Object dest = null; try { ByteArrayOutputStream bos = null; ObjectOutputStream oos = null; try { bos = new ByteArrayOutputStream(); oos = new ObjectOutputStream(bos); oos.writeObject(src); oos.flush(); } finally { oos.close(); } byte[] bytes = bos.toByteArray(); ObjectInputStream ois = null; try { ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); dest = ois.readObject(); } finally { ois.close(); } } catch(Exception e) { e.printStackTrace();//克隆失敗 } return dest; }
源對象類型及其成員對象類型需要實現Serializable介面,一個都不能少。
import java.io.Serializable; public class BattleShip implements Serializable { String name; ClonePilot pilot; BattleShip(String name, ClonePilot pilot) { this.name = name; this.pilot = pilot; } } //ClonePilot類型實現了Cloneable介面,不過這對通過Serializable方式拷貝對象沒有影響 public class ClonePilot implements Serializable,Cloneable { String name; String sex; ClonePilot(String name, String sex) { this.name = name; this.sex = sex; } public ClonePilot clone() { try { ClonePilot dest = (ClonePilot)super.clone(); return dest; } catch(Exception e) { e.printStackTrace(); } return null; } }
最後,執行測試代碼,查看結果。
public static void main(String[] args)
{ BattleShip bs = new BattleShip("Dominix", new ClonePilot("Alex", "male")); System.out.println(bs); System.out.println(bs.name + " "+bs.pilot.name); BattleShip cloneBs = (BattleShip)CloneObjUtils.cloneObjBySerialization(bs); System.out.println(cloneBs); System.out.println(cloneBs.name + " "+cloneBs.pilot.name); }
console--output--
cloneObject.BattleShip@154617c
Dominix Alex
cloneObject.BattleShip@cbcfc0
Dominix Alex
cloneObject.ClonePilot@a987ac
cloneObject.ClonePilot@1184fc6
從控制台的輸出可以看到,兩個不同的BattleShip對象,各自引用著不同的Clonepilot對象。String作為不可變類,這裡可以作為基本類型處理。該有的數據都有,兩個BattleShip對象也沒有引用同一個成員對象的情況。表示深拷貝成功了。
註意序列化會忽略transient修飾的變數。所以這種方式不會拷貝transient修飾的變數。
另外一種方式是Cloneable,核心是Object類的native方法clone()。通過調用clone方法,可以創建出一個當前對象的克隆體,但需要註意的是,這個方法不支持深拷貝。如果對象的成員變數是基礎類型,那妥妥的沒問題。但是對於自定義類型的變數或者集合(集合我還沒測試過)、數組,就有問題了。你會發現源對象和目標對象的自定義類型成員變數是同一個對象,也就是淺拷貝,淺拷貝就是對對象引用(地址)的拷貝。這樣的話源對象和目標對象就不是彼此獨立,而是糾纏不休了。為了彌補clone方法的這個不足。需要我們自己去處理非基本類型成員變數的深拷貝。
--Cloneable代碼
public class Cruiser implements Cloneable { String name; ClonePilot pilot; Cruiser(String name, ClonePilot pilot) { this.name = name; this.pilot = pilot; } //Object.clone方法是protected修飾的,無法在外部調用。所以這裡需要重載clone方法,改為public修飾,並且處理成員變數淺拷貝的問題。 public Cruiser clone() { try { Cruiser dest = (Cruiser)super.clone(); dest.pilot = this.pilot.clone(); return dest; } catch(Exception e) { e.printStackTrace(); } return null; } }
public class ClonePilot implements Serializable,Cloneable { String name; String sex; ClonePilot(String name, String sex) { this.name = name; this.sex = sex; } //因為所有成員變數都是基本類型,所以只需要調用Object.clone()即可 public ClonePilot clone() { try { ClonePilot dest = (ClonePilot)super.clone(); return dest; } catch(Exception e) { e.printStackTrace(); } return null; } }
下麵測試一下
public static void main(String[] args) { Cruiser cruiser = new Cruiser("VNI", new ClonePilot("Alex", "male")); System.out.println(cruiser); Cruiser cloneCruiser = cruiser.clone(); System.out.println(cloneCruiser); System.out.println(cruiser.pilot); System.out.println(cloneCruiser.pilot); System.out.println(cruiser.pilot.name); System.out.println(cloneCruiser.pilot.name); }
執行結果如下:
cloneObject.Cruiser@1eba861
cloneObject.Cruiser@1480cf9
cloneObject.ClonePilot@1496d9f
cloneObject.ClonePilot@3279cf
Alex
Alex
同樣,從控制台的輸出可以看到,兩個不同的Cruiser對象,各自引用著不同的Clonepilot對象。該有的數據都有,兩個Cruiser對象也沒有引用同一個成員對象的情況。表示深拷貝成功了。
工作中遇到的大多是Serializable方式,這種方式代碼量小,不容易出錯。使用Cloneable方式需要對源對象的數據結構有了足夠的瞭解才可以,代碼量大,涉及的文件也多。雖然他們都需要源對象類型及其引用的成員對象類型實現相應的介面,不過一般情況下問題也不大。但是我曾有幸遇到過一次需要深拷貝的場景,源對象的某個成員變數類型沒有實現任何介面,而且不允許我對此做任何修改。就在我黔驢技窮一籌莫展之際,我看到了光(kryo)。kryo是一個java序列化的框架,特別之處在於他不需要源對象類型實現任何介面,完美的解決了我的問題。後續我會寫一篇kryo框架的使用指南,敬請期待。(絕不咕咕)