對象複製的六種方法

題記:項目中用到對象的複製功能,自己寫了一個工具類,使用的淺克隆(當時根本不懂什麼淺克隆,深克隆),後期代碼評審被替換,雖潛心研究!特總結如下!

對象複製可以分爲:(地址複製),(實現Cloneable的方法),(使用BeanUtils.copyProperties() ),(PropertyUtils.copyProperties()),(序列化),(反射)

這裏先總結一下淺克隆和深克隆:

Java的數據類型分爲:基本數據類型(byte,short,int,long,float,double,boolean,char)和引用數據類型(數組,類,接口)。

在數據做爲參數傳遞的時候,基本數據類型是“值傳遞”,引用數據類型是“引用傳遞”(地址傳遞)。

這裏有一個特例:String是一個類,類是引用數據類型,做爲參數傳遞的時候,應該是引用傳遞。但是從結果看起來卻是值傳遞。

原因:String是被final修飾的,String的API中有這麼一句話:“their values cannot be changed after they are created”,
           意思是:String的值在創建之後不能被更改。
           API中還有一段:

          String str = "abc";
          等效於:
          char data[] = {'a', 'b', 'c'};
          String str = new String(data);
          也就是說:對String對象str的任何修改 等同於 重新創建一個對象,並將新的地址值賦值給str。

淺克隆和深克隆的主要區別在於是否支持引用類型的成員變量的複製。這裏說的複製是吧引用類型全部複製給對象,而不是把對象地址複製一份給對象。

一、淺克隆(地址複製)

public class ShallowCopy {

    @Data
    class Student {
        private String name;
    }

    //直接將對象的地址複製給另一個對象,但是這樣改變一個另一個也會改變!
    @Test
    public void test1() {
        Student stu1 = new Student();
        stu1.setName("aaaa");
        Student stu2 = stu1;
        stu1.setName("bbbb");
        stu2.setName("cccc");
        System.out.println("學生1:" + stu1.getName());
        System.out.println("學生2:" + stu2.getName());
    }
}

打印結果:

學生1:cccc
學生2:cccc
Disconnected from the target VM, address: '127.0.0.1:0', transport: 'socket'

Process finished with exit code 0

總結:這裏stu1和stu2在對內存中創建了兩個對象,兩個對象地址不同,Student  stu2=stu1;這段代碼將stu1的地址賦值給stu2,這是stu1和stu2指向同一個地址。當改變任意一個時兩個對象的值都會被改變。

二、深克隆(實現Cloneable的方法)

@Data
    class Student3 implements Cloneable {
        private int name;

        @Override
        public Object clone() {
            Student3 stu = null;
            try {
                stu = (Student3) super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return stu;
        }
    }

    @Test
    public void test3() {
        Student3 stu1 = new Student3();
        stu1.setName(12345);
        Student3 stu2 = (Student3) stu1.clone();

        System.out.println("學生1:" + stu1.getName());
        System.out.println("學生2:" + stu2.getName());

        stu2.setName(54321);

        System.out.println("學生1:" + stu1.getName());
        System.out.println("學生2:" + stu2.getName());
    }

打印結果:

Connected to the target VM, address: '127.0.0.1:0', transport: 'socket'
學生1:12345
學生2:12345
學生1:12345
學生2:54321
Disconnected from the target VM, address: '127.0.0.1:0', transport: 'socket'

Process finished with exit code 0

總結:可以看到這裏複製完以後對str2進行修改,但stu1的值沒有被修改,說明覆制stu2時進行了深克隆,而不是地址複製。

       這裏Student3實體實現了一個標記接口CloneAble,複寫的時Object方法的clone()方法。

延申:

如果對象中包含對象使用clone方法克隆。

@Data
    class Address implements Cloneable {
        private String add;

        @Override
        public Object clone() {
            Address stu = null;
            try {
                stu = (Address) super.clone();   //淺複製
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return stu;
        }
    }

    @Data
    class Student2 implements Cloneable {
        private int name;
        private Address addr;
        @Override
        public Object clone() {
            Student2 stu = null;
            try {
                stu = (Student2) super.clone();   //淺複製
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            //這裏需要添加這一條才能實現深克隆,否則即便實現了CloneAble接口依然是淺克隆
            stu.addr = (Address) addr.clone(); //深度複製
            return stu;
        }
    }

    @Test
    public void test4() {
        Address addr = new Address();
        addr.setAdd("北京");
        Student2 stu1 = new Student2();
        stu1.setName(123);
        stu1.setAddr(addr);

        Student2 stu2 = (Student2) stu1.clone();

        System.out.println("學生1:" + stu1.getName() + ",地址:" + stu1.getAddr().getAdd());
        System.out.println("學生2:" + stu2.getName() + ",地址:" + stu2.getAddr().getAdd());

        addr.setAdd("上海");

        System.out.println("學生1:" + stu1.getName() + ",地址:" + stu1.getAddr().getAdd());
        System.out.println("學生2:" + stu2.getName() + ",地址:" + stu2.getAddr().getAdd());
    }

總結:也就是兩個實體類對象都要實現serizable接口,另外在包含對象中要添加被包含對象的複製語句,否則深克隆不生效

//這裏需要添加這一條才能實現深克隆,否則即便實現了CloneAble接口依然是淺克隆
stu.addr = (Address) addr.clone(); //深度複製

三、深克隆(使用BeanUtils.copyProperties() )

    @Data
    class Student {
        private String name;
    }
     @Test
    public void test5() {
        Student stu1 = new Student();
        stu1.setName("aaaa");
        Student stu2 = new Student();
        BeanUtils.copyProperties(stu1, stu2);
//        stu2.setNumber(3456);
        System.out.println("stu1:" + stu1.getName());
        System.out.println("stu2:" + stu2.getName());
    }

四、深克隆(PropertyUtils.copyProperties())這裏對象複製完爲什麼String類型爲null,int類型爲0,有知道的留個言!

    @Data
    class Student {
        private String name;
    }
    @Test
    public void test6() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        Student stu1 = new Student();
        stu1.setName("aaaa");
        Student stu2 = new Student();
        PropertyUtils.copyProperties(stu2, stu1);
//        stu2.setNumber(2222);
        System.out.println("stu1:" + stu1.getName());
        System.out.println("stu2:" + stu2.getName());
    }

五、深克隆(序列化)

    @SuppressWarnings("unchecked")
    public static <T extends Serializable> T clone(T obj) {
        T cloneObj = null;
        try {
            //寫入字節流
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ObjectOutputStream obs = new ObjectOutputStream(out);
            obs.writeObject(obj);
            obs.close();

            //分配內存,寫入原始對象,生成新對象
            ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(ios);
            //返回生成的新對象
            cloneObj = (T) ois.readObject();
            ois.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cloneObj;
    }
    @Test
    public void test2() {
        Country country = new Country(1, "china");
        Person person = new Person(country, 1, "test");
        //引用傳遞
        Country country1 = country;
        Person person1 = person;
        //序列化和反序列化
        Person person2 = (Person) CloneUtils.clone(person);
        Country country2 = (Country) CloneUtils.clone(country);
        person1.setName("bbbbPerson");
        country1.setName("bbbContry");

        //修改序列化複製對象
        person2.setName("aaaaPerson");
        country2.setName("aaaaContry");

        System.out.println("創建國家       :" + country);
        System.out.println("引用傳遞國家  :" + country1);
        System.out.println("序列化複製國家  :" + country2);
        System.out.println("創建人         :" + person);
        System.out.println("引用傳遞人:" + person1);
        System.out.println("序列化複製人:" + person2);
    }
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Serializable {

    private static final long serialVersionUID = 1L;
    int id;
    String name;
    int age;
    Country country;

    public Person(Country country, int age, String name) {
        this.country = country;
        this.age = age;
        this.name = name;
    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Country implements Serializable {

    private static final long serialVersionUID = 1L;
    int code;
    String name;
}

總結:序列化就是將對象寫到流的過程,寫到流中的對象是原有對象的一個拷貝,而原對象仍然存在於內存中。通過序列化實現的拷貝不僅可以複製對象本身,而且可以複製其引用的成員對象,因此通過序列化將對象寫到一個流中,再從流裏將其讀出來,可以實現深克隆。需要注意的是能夠實現序列化的對象其類必須實現Serializable接口,否則無法實現序列化操作。

Java序列化是指把Java對象轉換爲字節序列的過程;而Java反序列化是指把字節序列恢復爲Java對象的過程。字節碼可以存儲,無狀態,而對象在內存中開闢空間,有地址。

由此,可以把對象序列化後反序列化。相當於破碎重組。

前提是:實體類需要實現序列化接口

六、深克隆(反射)

    @Test
    public void test1(){
        //2.創建一個靜態的Student對象
        Student stu1=new Student("ST20161282","Henry",22);
        try {
            Student stu2=(Student)ReflactCopyUtils.copyObj(stu1);
            stu2.setName("aaaa");
            System.out.println("複製對象成功");
            System.out.println(stu1.toString());
            System.out.println(stu2.toString());
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
public class ReflactCopyUtils {
    public static Object copyObj(Object obj) throws Exception {
        //3. 獲取已有Student對象的Class對象
        Class<?> classType = obj.getClass();
        //4. 通過Class.newinstance方法動態構建一個副本對象
        Object stu1 = classType.newInstance();
        //由於不知道源對象的具體屬性和屬性值,通過反射機制,先得到屬性名稱,再拼接字段得到屬性對應的get,set方法
        for (Field field : classType.getDeclaredFields()) {
            //5. 獲取副本對象的get,set方法
            String getMethodName = "get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
            String setMethodName = "set" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
            //6. 調用源對象的get方法獲取屬性值
            Method getMethod = classType.getDeclaredMethod(getMethodName, new Class[]{});
            Object value = getMethod.invoke(obj, new Object[]{});
            //7. 調用副本對象的set方法把屬性值複製過來
            Method setMethod = classType.getDeclaredMethod(setMethodName, new Class[]{field.getType()});
            setMethod.invoke(stu1, new Object[]{value});
        }
        return stu1;
    }
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    private String id;
    private String name;
    private int age;
}

總結:

案例介紹:

Student類,有屬性id, name, age, 還有對應的get,set方法和構造方法。
現產生一個Student對象。通過反射覆制此Student對象。複製時,並不知道源對象具體的屬性名稱。

案例設計

通過反射機制來獲取類的屬性和方法
通過反射生成對象,並通過反射來調用其set方法對屬性進行復制以達到複製對象的目的
最後對賦值成功的對象信息進行打印


方案實施

創建一個Student類
創建一個靜態的Student對象
獲取已有Student對象的Class對象
通過Class.newinstance方法動態構建一個副本對象
獲取副本對象的get,set方法
調用源對象的get方法獲取屬性值
調用副本對象的set方法把屬性值複製過來
打印輸出

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章