對象拷貝,是一個很是基礎的內容了,爲何會單獨的把這個領出來說解,主要是先前遇到了一個很是有意思的場景javascript
有一個任務,須要解析類xml標記語言,而後生成document對象,以後將會有一系列針對document對象的操做java
經過實際的測試,發現生成Document對象是比較耗時的一個操做,再加上這個任務場景中,須要解析的xml文檔是固定的幾個,那麼一個能夠優化的思路就是能不能緩存住建立後的Document對象,在實際使用的時候clone一份出來c++
看到了上面的應用背景,天然而言的就會想到深拷貝了,本篇博文則主要內容以下spring
深拷貝apache
至關於建立了一個新的對象,只是這個對象的全部內容,都和被拷貝的對象如出一轍而已,即二者的修改是隔離的,相互之間沒有影響數組
淺拷貝緩存
也是建立了一個對象,可是這個對象的某些內容(好比A)依然是被拷貝對象的,即經過這兩個對象中任意一個修改A,兩個對象的A都會受到影響工具
看到上面兩個簡單的說明,那麼問題來了性能
通常來講,淺拷貝方式須要實現Cloneable
接口,下面結合一個實例,來看下淺拷貝中哪些是獨立的,哪些是公用的測試
@Data public class ShallowClone implements Cloneable { private String name; private int age; private List<String> books; public ShallowClone clone() { ShallowClone clone = null; try { clone = (ShallowClone) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return clone; } public static void main(String[] args) { ShallowClone shallowClone = new ShallowClone(); shallowClone.setName("SourceName"); shallowClone.setAge(28); List<String> list = new ArrayList<>(); list.add("java"); list.add("c++"); shallowClone.setBooks(list); ShallowClone cloneObj = shallowClone.clone(); // 判斷兩個對象是否爲同一個對象(便是否是新建立了一個實例) System.out.println(shallowClone == cloneObj); // 修改一個對象的內容是否會影響另外一個對象 shallowClone.setName("newName"); shallowClone.setAge(20); shallowClone.getBooks().add("javascript"); System.out.println("source: " + shallowClone.toString() + "\nclone:" + cloneObj.toString()); shallowClone.setBooks(Arrays.asList("hello")); System.out.println("source: " + shallowClone.toString() + "\nclone:" + cloneObj.toString()); } }
輸出結果:
false source: ShallowClone(name=newName, age=20, books=[java, c++, javascript]) clone:ShallowClone(name=SourceName, age=28, books=[java, c++, javascript]) source: ShallowClone(name=newName, age=20, books=[hello]) clone:ShallowClone(name=SourceName, age=28, books=[java, c++, javascript])
結果分析:
其實,淺拷貝有個很是簡單的理解方式:
淺拷貝的整個過程就是,建立一個新的對象,而後新對象的每一個值都是由原對象的值,經過 =
進行賦值
這個怎麼理解呢?
上面的流程拆解就是:
- Object clone = new Object(); - clone.a = source.a - clone.b = source.b - ...
那麼=賦值有什麼特色呢?
基本數據類型是值賦值;非基本的就是引用賦值
深拷貝,就是要建立一個全新的對象,新的對象內部全部的成員也都是全新的,只是初始化的值已經由被拷貝的對象肯定了而已
那麼上面的實例改爲深拷貝應該是怎樣的呢?
能夠加上這麼一個方法
public ShallowClone deepClone() { ShallowClone clone = new ShallowClone(); clone.name = this.name; clone.age = this.age; if (this.books != null) { clone.books = new ArrayList<>(this.books); } return clone; } // 簡單改一下測試case public static void main(String[] args) { ShallowClone shallowClone = new ShallowClone(); shallowClone.setName("SourceName"); shallowClone.setAge(new Integer(1280)); List<String> list = new ArrayList<>(); list.add("java"); list.add("c++"); shallowClone.setBooks(list); ShallowClone cloneObj = shallowClone.deepClone(); // 判斷兩個對象是否爲同一個對象(便是否是新建立了一個實例) System.out.println(shallowClone == cloneObj); // 修改一個對象的內容是否會影響另外一個對象 shallowClone.setName("newName"); shallowClone.setAge(2000); shallowClone.getBooks().add("javascript"); System.out.println("source: " + shallowClone.toString() + "\nclone:" + cloneObj.toString()); shallowClone.setBooks(Arrays.asList("hello")); System.out.println("source: " + shallowClone.toString() + "\nclone:" + cloneObj.toString()); }
輸出結果爲:
false source: ShallowClone(name=newName, age=2000, books=[java, c++, javascript]) clone:ShallowClone(name=SourceName, age=1280, books=[java, c++]) source: ShallowClone(name=newName, age=2000, books=[hello]) clone:ShallowClone(name=SourceName, age=1280, books=[java, c++])
結果分析:
簡單來講,深拷貝是須要本身來實現的,對於基本類型能夠直接賦值,而對於對象、容器、數組來說,須要建立一個新的出來,而後從新賦值
深拷貝的用途咱們很容易能夠想見,某個複雜對象建立比較消耗資源的時候,就能夠緩存一個藍本,後續的操做都是針對深clone後的對象,這樣就不會出現混亂的狀況了
那麼淺拷貝呢?感受留着是一個坑,一我的修改了這個對象的值,結果發現對另外一我的形成了影響,真不是坑爹麼?
假設又這麼一個通知對象長下面這樣
private String notifyUser; // xxx private List<String> notifyRules;
咱們如今隨機挑選了一千我的,同時發送通知消息,因此須要建立一千個上面的對象,這些對象中呢,除了notifyUser不一樣,其餘的都同樣
在發送以前,忽然發現要臨時新增一條通知信息,若是是淺拷貝的話,只用在任意一個通知對象的notifyRules中添加一調消息,那麼這一千個對象的通知消息都會變成最新的了;而若是你是用深拷貝,那麼苦逼的得遍歷這一千個對象,每一個都加一條消息了
上面說到,淺拷貝,須要實現Clonebale接口,深拷貝通常須要本身來實現,那麼我如今拿到一個對象A,它本身沒有提供深拷貝接口,咱們除了主動一條一條的幫它實現以外,有什麼輔助工具可用麼?
對象拷貝區別與clone,它能夠支持兩個不一樣對象之間實現內容拷貝
Apache的兩個版本:(反射機制)
org.apache.commons.beanutils.PropertyUtils.copyProperties(Object dest, Object orig) org.apache.commons.beanutils.BeanUtils#cloneBean
Spring版本:(反射機制)
org.springframework.beans.BeanUtils.copyProperties(Object source, Object target, Class editable, String[] ignoreProperties)
cglib版本:(使用動態代理,效率高)
net.sf.cglib.beans.BeanCopier.copy(Object paramObject1, Object paramObject2, Converter paramConverter)
從上面的幾個有名的工具類來看,提供了兩種使用者姿式,一個是反射,一個是動態代理,下面分別來看兩種思路
經過反射的方式實現對象拷貝的思路仍是比較清晰的,先經過反射獲取對象的全部屬性,而後修改可訪問級別,而後賦值;再獲取繼承的父類的屬性,一樣利用反射進行賦值
上面的幾個開源工具,內部實現封裝得比較好,因此直接貼源碼可能不太容易一眼就能看出反射方式的原理,因此簡單的實現了一個, 僅提供思路
public static void copy(Object source, Object dest) throws Exception { Class destClz = dest.getClass(); // 獲取目標的全部成員 Field[] destFields = destClz.getDeclaredFields(); Object value; for (Field field : destFields) { // 遍歷全部的成員,並賦值 // 獲取value值 value = getVal(field.getName(), source); field.setAccessible(true); field.set(dest, value); } } private static Object getVal(String name, Object obj) throws Exception { try { // 優先獲取obj中同名的成員變量 Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); return field.get(obj); } catch (NoSuchFieldException e) { // 表示沒有同名的變量 } // 獲取對應的 getXxx() 或者 isXxx() 方法 name = name.substring(0, 1).toUpperCase() + name.substring(1); String methodName = "get" + name; String methodName2 = "is" + name; Method[] methods = obj.getClass().getMethods(); for (Method method : methods) { // 只獲取無參的方法 if (method.getParameterCount() > 0) { continue; } if (method.getName().equals(methodName) || method.getName().equals(methodName2)) { return method.invoke(obj); } } return null; }
上面的實現步驟仍是很是清晰的,首先是找同名的屬性,而後利用反射獲取對應的值
Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); return field.get(obj);
若是找不到,則找getXXX, isXXX來獲取
Cglib的BeanCopier就是經過代理的方式實現拷貝,性能優於反射的方式,特別是在大量的數據拷貝時,比較明顯
代理,咱們知道能夠區分爲靜態代理和動態代理,簡單來說就是你要操做對象A,可是你不直接去操做A,而是找一箇中轉porxyA, 讓它來幫你操做對象A
那麼這種技術是如何使用在對象拷貝的呢?
咱們知道,效率最高的對象拷貝方式就是Getter/Setter方法了,前面說的代理的含義指咱們不直接操做,而是找個中間商來賺差價,那麼方案就出來了
將原SourceA拷貝到目標DestB
實際上BeanCopier的思路大體如上,具體的方案固然就不太同樣了, 簡單看了一下實現邏輯,挺有意思的一塊,先留個坑,後面單獨開個博文補上
說明
從實現原理和經過簡單的測試,發現BeanCopier是掃描原對象的getXXX方法,而後賦值給同名的 setXXX 方法,也就是說,若是這個對象中某個屬性沒有get/set方法,那麼就沒法賦值成功了
深拷貝
至關於建立了一個新的對象,只是這個對象的全部內容,都和被拷貝的對象如出一轍而已,即二者的修改是隔離的,相互之間沒有影響
淺拷貝
也是建立了一個對象,可是這個對象的某些內容(好比A)依然是被拷貝對象的,即經過這兩個對象中任意一個修改A,兩個對象的A都會受到影響
經過反射方式實現對象拷貝
主要原理就是經過反射獲取全部的屬性,而後反射更改屬性的內容
經過代理實現對象拷貝
將原SourceA拷貝到目標DestB
建立一個代理 copyProxy 在代理中,依次調用 SourceA的get方法獲取屬性值,而後調用DestB的set方法進行賦值
盡信書則不如,已上內容,純屬一家之言,因本人能力通常,看法不全,若有問題,歡迎批評指正