1. 程式人生 > >java深拷貝淺拷貝

java深拷貝淺拷貝

將一個物件的引用複製給另外一個物件,一共有三種方式。第一種方式是直接賦值,第二種方式是淺拷貝,第三種是深拷貝。所以大家知道了哈,這三種概念實際上都是為了拷貝物件啊。

 

1、直接賦值

好,下面我們先看第一種方式,直接賦值。在Java中,A a1 = a2,我們需要理解的是這實際上覆制的是引用,也就是說a1和a2指向的是同一個物件。因此,當a1變化的時候,a2裡面的成員變數也會跟著變化。各位,請看下面的程式碼吧!

 

複製程式碼

/* 建立類 */  
class Resume {  
    private String name;  //姓名  
    private String sex;   //性別  
    private int age;      //年齡  
    private String experience; //工作經歷  
      
    public Resume(String name, String sex, int age) {  
        this.name = name;  
        this.sex = sex;  
        this.age = age;  
    }  
      
    public void setAge(int age) {  
        this.age = age;  
    }  
    public int getAge() {  
        return age;  
    }  
      
    public void setExperience(String experience) {  
        this.experience = experience;  
    }  
    public String getExperience() {  
        return experience;  
    }  
      
    public void displayResume() {  
        System.out.println("姓名:"+name+" 性別:"+sex+" 年齡:"+age);  
        System.out.println("工作經歷:"+experience);  
    }  
}  
  
public class MainClass {  
    public static void main(String[] args) {  
        Resume zhangsan = new Resume("zhangsan","男",24);  
        zhangsan.setExperience("2009-2013就讀於家裡蹲大學,精通JAVA,C,C++,C#等程式碼複製");  
        zhangsan.displayResume();  
        Resume zhangsan1 = zhangsan;  
        zhangsan1.setExperience("2009-2013就讀於家裡蹲大學,精通JAVA,C,C++,C#等");  
        zhangsan.displayResume();  
        zhangsan1.displayResume();  
    }  
}  

複製程式碼

程式執行結果

複製程式碼

姓名:zhangsan 性別:男 年齡:24  
工作經歷:2009-2013就讀於家裡蹲大學,精通JAVA,C,C++,C#等程式碼複製  
姓名:zhangsan 性別:男 年齡:24  
工作經歷:2009-2013就讀於家裡蹲大學,精通JAVA,C,C++,C#等  
姓名:zhangsan 性別:男 年齡:24  
工作經歷:2009-2013就讀於家裡蹲大學,精通JAVA,C,C++,C#等  

複製程式碼

在本程式中,生成了一份zhangsan的簡歷。之後又複製了一份簡歷zhangsan1,可見zhangsan1中工作經歷發生變化時,zhangsan的工作經歷也發生了變化。

 

2、淺拷貝

上面直接賦值的結果,有時候可能並不是我們所想要的。就像我們投簡歷的時候,可能會根據應聘公司的型別做出相應的調整,如果是投技術類的工作可能會偏技術一點;如果是投國企啊什麼之類的,社會經歷學生工作什麼的可能也是很重要的一部分。所以我們不需要當我們修改一份簡歷的時候,所有的簡歷都變調。不然到時候投技術類的公司又得改回來。說了這麼多,我們也就是希望,把a1賦值給a2之後,a1和a2能保持獨立,不要互相影響。

 

實現上面想法之一的方法就是Object的Clone()函數了。在這裡,我們需要了解clone()主要做了些什麼,建立一個新物件,然後將當前物件的非靜態欄位複製到該新物件,如果欄位是值型別的,那麼對該欄位執行復制;如果該欄位是引用型別的話,則複製引用但不復制引用的物件。因此,原始物件及其副本引用同一個物件。

好,我們先看這一段話的前一部分,如果欄位是值型別,則直接複製。如下面程式所示

 

複製程式碼

/* 建立類,實現Clone方法  */  
class Resume  implements Cloneable{  
    private String name;  //姓名  
    private String sex;   //性別  
    private int age;      //年齡  
    private String experience; //工作經歷  
      
    public Resume(String name, String sex, int age) {  
        this.name = name;  
        this.sex = sex;  
        this.age = age;  
    }  
      
    public void setAge(int age) {  
        this.age = age;  
    }  
    public int getAge() {  
        return age;  
    }  
      
    public void setExperience(String experience) {  
        this.experience = experience;  
    }  
    public String getExperience() {  
        return experience;  
    }  
      
    public void displayResume() {  
        System.out.println("姓名:"+name+" 性別:"+sex+" 年齡:"+age);  
        System.out.println("工作經歷:"+experience);  
    }  
      
    public Object clone() {  
        try {  
            return (Resume)super.clone();  
        } catch (Exception e) {  
            e.printStackTrace();  
            return null;  
        }  
    }  
}  
  
public class MainClass {  
    public static void main(String[] args) {  
        Resume zhangsan = new Resume("zhangsan","男",24);  
        zhangsan.setExperience("2009-2013就讀於家裡蹲大學,精通JAVA,C,C++,C#等程式碼拷貝和貼上");  
        zhangsan.displayResume();  
        Resume zhangsan1 = (Resume)zhangsan.clone();  
        zhangsan1.setAge(23);  
        zhangsan1.displayResume();  
        Resume zhangsan2 = (Resume)zhangsan.clone();  
        zhangsan2.setExperience("2009-2013就讀於家裡蹲大學,精通JAVA,C,C++,C#等程式碼");  
        zhangsan2.displayResume();  
        zhangsan.displayResume();  
    }  
}  

複製程式碼

程式執行結果

複製程式碼

姓名:zhangsan 性別:男 年齡:24  
工作經歷:2009-2013就讀於家裡蹲大學,精通JAVA,C,C++,C#等程式碼拷貝和貼上  
姓名:zhangsan 性別:男 年齡:23  
工作經歷:2009-2013就讀於家裡蹲大學,精通JAVA,C,C++,C#等程式碼拷貝和貼上  
姓名:zhangsan 性別:男 年齡:24  
工作經歷:2009-2013就讀於家裡蹲大學,精通JAVA,C,C++,C#等程式碼  
姓名:zhangsan 性別:男 年齡:24  
工作經歷:2009-2013就讀於家裡蹲大學,精通JAVA,C,C++,C#等程式碼拷貝和貼上  

複製程式碼

由程式的執行結果可以看出,我們實現了a1和a2引用的獨立。

 

但是什麼叫“如果該欄位是引用型別的話,則複製引用但不復制引用的物件。因此,原始物件及其副本引用同一個物件。”,到底什麼意思?不用著急,我們接下來看下面一段程式:

複製程式碼

class Experience {  
      
    private String educationBackground;  
    private String skills;  
      
    public void setExperience(String educationBackground, String skills) {  
        // TODO Auto-generated constructor stub  
        this.educationBackground = educationBackground;  
        this.skills = skills;  
    }  
    public String toString() {  
        return educationBackground + skills;  
    }  
}  
  
/* 建立類,實現Clone方法  */  
class Resume  implements Cloneable{  
    private String name;  //姓名  
    private String sex;   //性別  
    private int age;      //年齡  
    private Experience experience; //工作經歷  
      
    public Resume(String name, String sex, int age) {  
        this.name = name;  
        this.sex = sex;  
        this.age = age;  
        this.experience = new Experience();  
    }  
      
    public void setAge(int age) {  
        this.age = age;  
    }  
    public int getAge() {  
        return age;  
    }  
      
    public Experience getExperience() {  
        return experience;  
    }  
      
    public void setExperience(String educationBackground, String skills) {  
        experience.setExperience(educationBackground, skills);  
    }  
      
    public void displayResume() {  
        System.out.println("姓名:"+name+" 性別:"+sex+" 年齡:"+age);  
        System.out.println("工作經歷:"+experience.toString());  
    }  
      
    public Object clone() {  
        try {  
            return (Resume)super.clone();  
        } catch (Exception e) {  
            e.printStackTrace();  
            return null;  
        }  
    }  
}  
  
public class MainClass {  
    public static void main(String[] args) {  
        Resume zhangsan = new Resume("zhangsan","男",24);  
        zhangsan.setExperience("2009-2013就讀於家裡蹲大學","精通JAVA,C,C++,C#等程式碼拷貝和貼上");  
        zhangsan.displayResume();  
  
        Resume zhangsan2 = (Resume)zhangsan.clone();  
        zhangsan2.setExperience("2009-2013就讀於家裡蹲大學","精通JAVA,C,C++,C#等");  
        zhangsan2.displayResume();  
        zhangsan.displayResume();  
        zhangsan2.displayResume();  
    }  
}  

複製程式碼

程式執行結果:

複製程式碼

姓名:zhangsan 性別:男 年齡:24  
工作經歷:2009-2013就讀於家裡蹲大學精通JAVA,C,C++,C#等程式碼拷貝和貼上  
姓名:zhangsan 性別:男 年齡:24  
工作經歷:2009-2013就讀於家裡蹲大學精通JAVA,C,C++,C#等  
姓名:zhangsan 性別:男 年齡:24  
工作經歷:2009-2013就讀於家裡蹲大學精通JAVA,C,C++,C#等  
姓名:zhangsan 性別:男 年齡:24  
工作經歷:2009-2013就讀於家裡蹲大學精通JAVA,C,C++,C#等  

複製程式碼

我們看一下上面兩段程式差異在哪兒,第一段程式的工作經歷是作為Resume類的一個普通的成員變數,也就是值屬性。而後面一段程式中,工作經歷Experience是一個類。結合上面程式的執行結果,我們再來理解“如果該欄位是引用型別的話,則複製引用但不復制引用的物件。因此,原始物件及其副本引用同一個物件。”其實也就是說,zhangsan和zhangsan2裡面的Experience類指向的是同一個物件嘛!那不管是zhangsan裡面的Experience變化,還是zhangsan2裡面的Experience變化都會影響另外一個啊。

 

3、深拷貝

其實出現問題的關鍵就在於clone()方法上,我們知道該clone()方法是使用Object類的clone()方法,但是該方法存在一個缺陷,它並不會將物件的所有屬性全部拷貝過來,而是有選擇性的拷貝,基本規則如下:

      1、 基本型別

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

      2、 物件

         如果變數是一個例項物件,則拷貝其地址引用,也就是說此時新物件與原來物件是公用該例項變數。

      3、 String字串

         若變數為String字串,則拷貝其地址引用。但是在修改時,它會從字串池中重新生成一個新的字串,原有紫都城物件保持不變。

      基於上面上面的規則,我們很容易發現問題的所在,他們三者公用一個物件,張三修改了該郵件內容,則李四和王五也會修改,所以才會出現上面的情況。對於這種情況我們還是可以解決的,只需要在clone()方法裡面新建一個物件,然後張三引用該物件即可:

複製程式碼

rotected 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提供的一種簡單的拷貝機制,不便於直接使用。

      對於上面的解決方案還是存在一個問題,若我們系統中存在大量的物件是通過拷貝生成的,如果我們每一個類都寫一個clone()方法,並將還需要進行深拷貝,新建大量的物件,這個工程是非常大的,這裡我們可以利用序列化來實現物件的拷貝。

 

如何利用序列化來完成物件的拷貝呢?在記憶體中通過位元組流的拷貝是比較容易實現的。把母物件寫入到一個位元組流中,再從位元組流中將其讀出來,這樣就可以建立一個新的物件了,並且該新物件與母物件之間並不存在引用共享的問題,真正實現物件的深拷貝。

複製程式碼

public class CloneUtils {
    @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;
    }
}

複製程式碼

使用該工具類的物件必須要實現Serializable介面,否則是沒有辦法實現克隆的。

複製程式碼

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

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

}

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

複製程式碼

所以使用該工具類的物件只要實現Serializable介面就可實現物件的克隆,無須繼承Cloneable介面實現clone()方法。

複製程式碼

public class Client {
    public static void main(String[] args) {
        //寫封郵件
        Email email = new Email("請參加會議","請與今天12:30到二會議室參加會議...");
        
        Person person1 =  new Person("張三",email);
        
        Person person2 =  CloneUtils.clone(person1);
        person2.setName("李四");
        Person person3 =  CloneUtils.clone(person1);
        person3.setName("王五");
        person1.getEmail().setContent("請與今天12:00到二會議室參加會議...");
        
        System.out.println(person1.getName() + "的郵件內容是:" + person1.getEmail().getContent());
        System.out.println(person2.getName() + "的郵件內容是:" + person2.getEmail().getContent());
        System.out.println(person3.getName() + "的郵件內容是:" + person3.getEmail().getContent());
    }
}
-------------------
Output:
張三的郵件內容是:請與今天12:00到二會議室參加會議...
李四的郵件內容是:請與今天12:30到二會議室參加會議...
王五的郵件內容是:請與今天12:30到二會議室參加會議...