鍍金池/ 教程/ Java/ 使用序列化實(shí)現(xiàn)對(duì)象的拷貝
Java 集合細(xì)節(jié)(四):保持 compareTo 和 equals 同步
Iterator
使用序列化實(shí)現(xiàn)對(duì)象的拷貝
fail-fast 機(jī)制
關(guān)鍵字 final
Vector
HashTable
Java 集合細(xì)節(jié)(一):請(qǐng)為集合指定初始容量
強(qiáng)制類型轉(zhuǎn)換
數(shù)組之一:認(rèn)識(shí) JAVA 數(shù)組
Java 集合細(xì)節(jié)(三):subList 的缺陷
hashCode
ArrayList
數(shù)組之二
List 總結(jié)
LinkedList
Java 提高篇(九)—–實(shí)現(xiàn)多重繼承
Java 的四舍五入
關(guān)鍵字 static
理解 Java 的三大特性之多態(tài)
抽象類與接口
集合大家族
異常(二)
Java 集合細(xì)節(jié)(二):asList 的缺陷
Map 總結(jié)
TreeSet
equals() 方法總結(jié)
Java 提高篇(十)—–詳解匿名內(nèi)部類
HashMap
Stack
詳解內(nèi)部類
TreeMap
異常(一)
詳解 Java 定時(shí)任務(wù)
HashSet
字符串
理解 Java 的三大特性之繼承
理解 Java 的三大特性之封裝
代碼塊

使用序列化實(shí)現(xiàn)對(duì)象的拷貝

我們知道在 Java 中存在這個(gè)接口 Cloneable,實(shí)現(xiàn)該接口的類都會(huì)具備被拷貝的能力,同時(shí)拷貝是在內(nèi)存中進(jìn)行,在性能方面比我們直接通過(guò) new 生成對(duì)象來(lái)的快,特別是在大對(duì)象的生成上,使得性能的提升非常明顯。然而我們知道拷貝分為深拷貝和淺拷貝之分,但是淺拷貝存在對(duì)象屬性拷貝不徹底問(wèn)題。關(guān)于深拷貝、淺拷貝的請(qǐng)參考這里:漸析 java 的淺拷貝和深拷貝

一、淺拷貝問(wèn)題

我們先看如下代碼:


    public class Person implements Cloneable{
        /** 姓名 **/
        private String name;

        /** 電子郵件 **/
        private Email email;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Email getEmail() {
            return email;
        }

        public void setEmail(Email email) {
            this.email = email;
        }

        public Person(String name,Email email){
            this.name  = name;
            this.email = email;
        }

        public Person(String name){
            this.name = name;
        }

        protected Person clone() {
            Person person = null;
            try {
                person = (Person) super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }

            return person;
        }
    }

    public class Client {
        public static void main(String[] args) {
            //寫封郵件
            Email email = new Email("請(qǐng)參加會(huì)議","請(qǐng)與今天12:30到二會(huì)議室參加會(huì)議...");

            Person person1 =  new Person("張三",email);

            Person person2 =  person1.clone();
            person2.setName("李四");
            Person person3 =  person1.clone();
            person3.setName("王五");

            System.out.println(person1.getName() + "的郵件內(nèi)容是:" + person1.getEmail().getContent());
            System.out.println(person2.getName() + "的郵件內(nèi)容是:" + person2.getEmail().getContent());
            System.out.println(person3.getName() + "的郵件內(nèi)容是:" + person3.getEmail().getContent());
        }
    }
    --------------------
    Output:
    張三的郵件內(nèi)容是:請(qǐng)與今天12:30到二會(huì)議室參加會(huì)議...
    李四的郵件內(nèi)容是:請(qǐng)與今天12:30到二會(huì)議室參加會(huì)議...
    王五的郵件內(nèi)容是:請(qǐng)與今天12:30到二會(huì)議室參加會(huì)議...

在該應(yīng)用程序中,首先定義一封郵件,然后將該郵件發(fā)給張三、李四、王五三個(gè)人,由于他們是使用相同的郵件,并且僅有名字不同,所以使用張三該對(duì)象類拷貝李四、王五對(duì)象然后更改下名字即可。程序一直到這里都沒有錯(cuò),但是如果我們需要張三提前 30 分鐘到,即把郵件的內(nèi)容修改下:


    public class Client {
        public static void main(String[] args) {
            //寫封郵件
            Email email = new Email("請(qǐng)參加會(huì)議","請(qǐng)與今天12:30到二會(huì)議室參加會(huì)議...");

            Person person1 =  new Person("張三",email);

            Person person2 =  person1.clone();
            person2.setName("李四");
            Person person3 =  person1.clone();
            person3.setName("王五");

            person1.getEmail().setContent("請(qǐng)與今天12:00到二會(huì)議室參加會(huì)議...");

            System.out.println(person1.getName() + "的郵件內(nèi)容是:" + person1.getEmail().getContent());
            System.out.println(person2.getName() + "的郵件內(nèi)容是:" + person2.getEmail().getContent());
            System.out.println(person3.getName() + "的郵件內(nèi)容是:" + person3.getEmail().getContent());
        }
    }

在這里同樣是使用張三該對(duì)象實(shí)現(xiàn)對(duì)李四、王五拷貝,最后將張三的郵件內(nèi)容改變?yōu)椋赫?qǐng)與今天 12:00 到二會(huì)議室參加會(huì)議…。但是結(jié)果是:


    張三的郵件內(nèi)容是:請(qǐng)與今天12:00到二會(huì)議室參加會(huì)議...
    李四的郵件內(nèi)容是:請(qǐng)與今天12:00到二會(huì)議室參加會(huì)議...
    王五的郵件內(nèi)容是:請(qǐng)與今天12:00到二會(huì)議室參加會(huì)議...

這里我們就疑惑了為什么李四和王五的郵件內(nèi)容也發(fā)送了改變呢?讓他們提前30分鐘到人家會(huì)有意見的!

其實(shí)出現(xiàn)問(wèn)題的關(guān)鍵就在于 clone() 方法上,我們知道該 clone() 方法是使用 Object 類的 clone() 方法,但是該方法存在一個(gè)缺陷,它并不會(huì)將對(duì)象的所有屬性全部拷貝過(guò)來(lái),而是有選擇性的拷貝,基本規(guī)則如下:

1、基本類型

如果變量是基本很類型,則拷貝其值,比如 int、float 等。

2、對(duì)象

如果變量是一個(gè)實(shí)例對(duì)象,則拷貝其地址引用,也就是說(shuō)此時(shí)新對(duì)象與原來(lái)對(duì)象是公用該實(shí)例變量。

3、String 字符串

若變量為 String 字符串,則拷貝其地址引用。但是在修改時(shí),它會(huì)從字符串池中重新生成一個(gè)新的字符串,原有紫都城對(duì)象保持不變。

基于上面上面的規(guī)則,我們很容易發(fā)現(xiàn)問(wèn)題的所在,他們?nèi)吖靡粋€(gè)對(duì)象,張三修改了該郵件內(nèi)容,則李四和王五也會(huì)修改,所以才會(huì)出現(xiàn)上面的情況。對(duì)于這種情況我們還是可以解決的,只需要在 clone() 方法里面新建一個(gè)對(duì)象,然后張三引用該對(duì)象即可:


    protected Person clone() {
            Person person = null;
            try {
                person = (Person) super.clone();
                person.setEmail(new Email(person.getEmail().getObject(),person.getEmail().getContent()));
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }

            return person;
        }

所以:淺拷貝只是 Java 提供的一種簡(jiǎn)單的拷貝機(jī)制,不便于直接使用。

對(duì)于上面的解決方案還是存在一個(gè)問(wèn)題,若我們系統(tǒng)中存在大量的對(duì)象是通過(guò)拷貝生成的,如果我們每一個(gè)類都寫一個(gè) clone() 方法,并將還需要進(jìn)行深拷貝,新建大量的對(duì)象,這個(gè)工程是非常大的,這里我們可以利用序列化來(lái)實(shí)現(xiàn)對(duì)象的拷貝。

二、利用序列化實(shí)現(xiàn)對(duì)象的拷貝

如何利用序列化來(lái)完成對(duì)象的拷貝呢?在內(nèi)存中通過(guò)字節(jié)流的拷貝是比較容易實(shí)現(xiàn)的。把母對(duì)象寫入到一個(gè)字節(jié)流中,再?gòu)淖止?jié)流中將其讀出來(lái),這樣就可以創(chuàng)建一個(gè)新的對(duì)象了,并且該新對(duì)象與母對(duì)象之間并不存在引用共享的問(wèn)題,真正實(shí)現(xiàn)對(duì)象的深拷貝。


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

                //分配內(nèi)存,寫入原始對(duì)象,生成新對(duì)象
                ByteArrayInputStream ios = new  ByteArrayInputStream(out.toByteArray());
                ObjectInputStream ois = new ObjectInputStream(ios);
                //返回生成的新對(duì)象
                cloneObj = (T) ois.readObject();
                ois.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return cloneObj;
        }
    }

使用該工具類的對(duì)象必須要實(shí)現(xiàn) Serializable 接口,否則是沒有辦法實(shí)現(xiàn)克隆的。


    public class Person implements Serializable{
        private static final long serialVersionUID = 2631590509760908280L;

        ..................
        //去除clone()方法

    }

    public class Email implements Serializable{
        private static final long serialVersionUID = 1267293988171991494L;

        ....................
    }

所以使用該工具類的對(duì)象只要實(shí)現(xiàn) Serializable 接口就可實(shí)現(xiàn)對(duì)象的克隆,無(wú)須繼承 Cloneable 接口實(shí)現(xiàn) clone() 方法。


    public class Client {
        public static void main(String[] args) {
            //寫封郵件
            Email email = new Email("請(qǐng)參加會(huì)議","請(qǐng)與今天12:30到二會(huì)議室參加會(huì)議...");

            Person person1 =  new Person("張三",email);

            Person person2 =  CloneUtils.clone(person1);
            person2.setName("李四");
            Person person3 =  CloneUtils.clone(person1);
            person3.setName("王五");
            person1.getEmail().setContent("請(qǐng)與今天12:00到二會(huì)議室參加會(huì)議...");

            System.out.println(person1.getName() + "的郵件內(nèi)容是:" + person1.getEmail().getContent());
            System.out.println(person2.getName() + "的郵件內(nèi)容是:" + person2.getEmail().getContent());
            System.out.println(person3.getName() + "的郵件內(nèi)容是:" + person3.getEmail().getContent());
        }
    }
    -------------------
    Output:
    張三的郵件內(nèi)容是:請(qǐng)與今天12:00到二會(huì)議室參加會(huì)議...
    李四的郵件內(nèi)容是:請(qǐng)與今天12:30到二會(huì)議室參加會(huì)議...
    王五的郵件內(nèi)容是:請(qǐng)與今天12:30到二會(huì)議室參加會(huì)議...

鞏固基礎(chǔ),提高技術(shù),不懼困難,攀登高峰?。。。。?!

參考文獻(xiàn)《編寫高質(zhì)量代碼 改善Java程序的151個(gè)建議》—-秦小波