1. 程式人生 > >淺談BeanUtils的拷貝,深度克隆

淺談BeanUtils的拷貝,深度克隆

1、BeanUtil本地簡單測試
在專案中由於需要對某些物件進行深度拷貝然後進行持久化操作,想到了apache和spring都提供了BeanUtils的深度拷貝工具包,自己寫了幾個Demo做測試,定義了兩個類User和Person,其中User的屬性引用了Person類。

複製程式碼

public class User {
    private int id;
    private String username;// 使用者姓名
    private String sex;// 性別
    private Date birthday;// 生日
    private String address;// 地址

    private Person person; //包裝類

    //get set方法此處省略
}

複製程式碼

複製程式碼

//Person類
public class Person {
    private int id;
    private String userName ;
    private int age ;
    private String mobilePhone ;
    public  Person(){}
    public Person(int id,String userName, int age, String mobilePhone) {
        this.id = id;
        this.userName = userName;
        this.age = age;
        this.mobilePhone = mobilePhone;
    }

    //get set方法此處省略
}

複製程式碼

編寫測試方法進行調研,主要是檢視物件中包裝的物件是否引用了同一個地址,從而判斷是否是深度拷貝還是淺拷貝

複製程式碼

@Test
    public void CopyTest(){
        User user=new User();
        user.setId(1);
        user.setSex("man");
        user.setUsername("Tison");
        user.setAddress("address");
        user.setBirthday(new Date());
        Person p=new Person();
        p.setUserName("p1");
        user.setPerson(p);
        User target=new User();
        BeanUtils.copyProperties(user,target);
        System.out.println(target.getAddress()==user.getAddress());
        System.out.println(target.getPerson()==user.getPerson());
        System.out.println(user.toString());
        System.out.println(target.toString());
    }

複製程式碼

列印結果:

1

2

3

4

false  (String屬性的記憶體地址不相等)

false  (包裝物件的記憶體地址不相等)

src.main.mybatis.User@7907ec20

src.main.mybatis.User@546a03af

兩個物件的雜湊碼不相等,引用物件的地址也不相同,並且對包裝物件的操作都是互不影響,簡單測試下可以看到BeanUtils實現了深度拷貝的效果。

2、專案測試
但是到了本人所做的專案中,BeanUtils的效果就不是深度拷貝了,用虛擬碼進行簡單說明:

複製程式碼

//source為A物件,target為B物件
BeanUtils.copyProperties(source,target);
//呼叫setSecret方法將B物件的某型包裝屬性set為null
setSecret(target);
//分別列印對比的結果
System.out.println(target.getUserInfo()==source.getUserInfo());
System.out.println(source.getUserInfo().hashCode());
System.out.println(target.getUserInfo().hashCode());

複製程式碼

1

2

3

4

//列印測試結果

true

1589531316

1589531316

兩份物件裡的包裝物件記憶體地址比較結果為true,而且物件的雜湊嗎指向了同一位置。
顯而易見,BeanUtils並未進行深度拷貝。本人在專案中正因為採用了BeanUtils的拷貝方法,在對兩份物件的不同操作時都會互相影響導致持久化的異常,可見基於BeanUtils的拷貝方法並不是萬能的,而且由於原始碼中採用反射機制,其效能也被許多博主詬病,在網上進行了綜合調研,發現BeanUtils的copyProperties()方法的確存在著淺拷貝的情況,這對於持久化操作實體類的時候是很大的一個坑,那麼最靠譜的深拷貝方法還是要序列化後寫流的方法,只是該方法需要實現Serializable介面。

3、深拷貝
深複製(深克隆)被複制物件的所有變數都含有與原來的物件相同的值,除去那些引用其他物件的變數,那些引用其他物件的變數將指向被複制過的新物件,而不再試原有的那些被引用的物件,換言之,深複製把要複製的物件所引用的物件都複製了一遍。
把物件寫到流裡的過程是序列化(Serilization)過程,但是在Java程式師圈子裡又非常形象地稱為“冷凍”或者“醃鹹菜(picking)”過程;而把物件從流中讀出來的並行化(Deserialization)過程則叫做“解凍”或者“回鮮(depicking)”過程。應當指出的是,寫在流裡的是物件的一個拷貝,而原物件仍然存在於JVM裡面,因此“醃成鹹菜”的只是物件的一個拷貝,Java鹹菜還可以回鮮。在Java語言裡深複製一個物件,常常可以先使物件實現Serializable介面,然後把物件(實際上只是物件的一個拷貝)寫到一個流裡(醃成鹹菜),再從流裡讀出來(把鹹菜回鮮),便可以重建物件。

在專案中我們需要克隆的物件可能包含多層引用型別,這就要涉及到多層克隆問題,多層克隆不僅要將克隆物件實現序列化介面,引用物件也同樣的要實現序列化介面:

複製程式碼

public class User implements Serializable{
    private int id;
    private String username;// 使用者姓名
    private String sex;// 性別
    private Date birthday;// 生日
    private String address;// 地址
    private Person person; //引用型別

    public User myColon(){
        User copy=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);
            copy = (User) ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
            return copy;
    }

    //此處省略get-set方法程式碼
}

複製程式碼

引用型別也需要實現Serializable介面,否則會序列化失敗。

複製程式碼

public class Person implements Serializable {
    private int id;
    private String userName ;
    private int age ;
    private String mobilePhone ;
    public  Person(){}
    public Person(int id,String userName, int age, String mobilePhone) {
        this.id = id;
        this.userName = userName;
        this.age = age;
        this.mobilePhone = mobilePhone;
    }
    //此處省略get-set方法
}

複製程式碼

結論:

1

2

3

結論:

1、BeanUtils的copyProperties()方法並不是完全的深度克隆,在包含有引用型別的物件拷貝上就可能會出現引用物件指向同一個的情況,且該方法的效能低下,專案中一定要謹慎使用。

2、要實現高效能且安全的深度克隆方法還是實現Serializable介面,多層克隆時,引用型別均要實現Serializable介面。

 

 

 

IV. 小結
1. 深拷貝和淺拷貝
深拷貝

相當於建立了一個新的物件,只是這個物件的所有內容,都和被拷貝的物件一模一樣而已,即兩者的修改是隔離的,相互之間沒有影響 
- 完全獨立

淺拷貝

也是建立了一個物件,但是這個物件的某些內容(比如A)依然是被拷貝物件的,即通過這兩個物件中任意一個修改A,兩個物件的A都會受到影響

等同與新建立一個物件,然後使用=,將原物件的屬性賦值給新物件的屬性
需要實現Cloneable介面
2. 物件拷貝的兩種方法
通過反射方式實現物件拷貝

主要原理就是通過反射獲取所有的屬性,然後反射更改屬性的內容

通過代理實現物件拷貝

將原SourceA拷貝到目標DestB

建立一個代理 copyProxy 
在代理中,依次呼叫 SourceA的get方法獲取屬性值,然後呼叫DestB的set方法進行賦值