1. 程式人生 > >Java物件的複製三種方式

Java物件的複製三種方式

我們都是愛情的草,在寒冷中死去,在溫暖中重生,四季輪迴,為了愛情不計仇恨。——《我有一杯酒,可以蔚風塵》

1、概述

在實際程式設計過程中,我們常常要遇到這種情況:有一個物件A,在某一時刻A中已經包含了一些有效值,此時可能 會需要一個和A完全相同新物件B,並且此後對B任何改動都不會影響到A中的值,也就是說,A與B是兩個獨立的物件,但B的初始值是由A物件確定的。例如下面程式展示的情況:

class Student {  
    private int number;  

    public int getNumber() {  
        return number;  
    }  

    public
void setNumber(int number) { this.number = number; } } public class Test { public static void main(String args[]) { Student stu1 = new Student(); stu1.setNumber(12345); Student stu2 = stu1; stu1.setNumber(54321); System.out
.println("學生1:" + stu1.getNumber()); System.out.println("學生2:" + stu2.getNumber()); } }

結果:

學生1:54321
學生2:54321

為什麼改變學生2的學號,學生1的學號也發生了變化呢?
原因出在(stu2 = stu1) 這一句。該語句的作用是將stu1的引用賦值給stu2,
這樣,stu1和stu2指向記憶體堆中同一個物件。如圖:

這裡寫程式碼片

那麼,怎麼能幹乾淨淨清清楚楚地複製一個物件呢。在 Java語言中,用簡單的賦值語句是不能滿足這種需求的。要滿足這種需求有很多途徑,
(1)將A物件的值分別通過set方法加入B物件中;
(2)通過重寫java.lang.Object類中的方法clone();
(3)通過org.apache.commons中的工具類BeanUtils和PropertyUtils進行物件複製;
(4)通過序列化實現物件的複製。

2、將A物件的值分別通過set方法加入B物件中

對屬性逐個賦值,本例項為了演示簡單就設定了一個屬性:

Student stu1 = new Student();  
stu1.setNumber(12345);  
Student stu2 = new Student();  
stu2.setNumber(stu1.getNumber());

我們發現,屬性少對屬性逐個賦值還挺方便,但是屬性多時,就需要一直get、set了,非常麻煩。

3、重寫java.lang.Object類中的方法clone()

先介紹一下兩種不同的克隆方法,淺克隆(ShallowClone)和深克隆(DeepClone)。

在Java語言中,資料型別分為值型別(基本資料型別)和引用型別,值型別包括int、double、byte、boolean、char等簡單資料型別,引用型別包括類、介面、陣列等複雜型別。淺克隆和深克隆的主要區別在於是否支援引用型別的成員變數的複製,下面將對兩者進行詳細介紹。

3.1 淺克隆

一般步驟:

  1. 被複制的類需要實現Clonenable介面(不實現的話在呼叫clone方法會丟擲CloneNotSupportedException異常), 該介面為標記介面(不含任何方法)

  2. 覆蓋clone()方法,訪問修飾符設為public。方法中呼叫super.clone()方法得到需要的複製物件。(native為本地方法)

class Student implements Cloneable{  
    private int number;  

    public int getNumber() {  
        return number;  
    }  

    public void setNumber(int number) {  
        this.number = number;  
    }  

    @Override  
    public Object clone() {  
        Student stu = null;  
        try{  
            stu = (Student)super.clone();  
        }catch(CloneNotSupportedException e) {  
            e.printStackTrace();  
        }  
        return stu;  
    }  
}  
public class Test {  
    public static void main(String args[]) {  
        Student stu1 = new Student();  
        stu1.setNumber(12345);  
        Student stu2 = (Student)stu1.clone();  

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

        stu2.setNumber(54321);  

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

結果:
學生1:12345
學生2:12345
學生1:12345
學生2:54321

在淺克隆中,如果原型物件的成員變數是值型別,將複製一份給克隆物件;如果原型物件的成員變數是引用型別,則將引用物件的地址複製一份給克隆物件,也就是說原型物件和克隆物件的成員變數指向相同的記憶體地址。

簡單來說,在淺克隆中,當物件被複制時只複製它本身和其中包含的值型別的成員變數,而引用型別的成員物件並沒有複製。

這裡寫圖片描述
在Java語言中,通過覆蓋Object類的clone()方法可以實現淺克隆。

3.2 深克隆

package abc;  

class Address implements Cloneable {  
    private String add;  

    public String getAdd() {  
        return add;  
    }  

    public void setAdd(String add) {  
        this.add = add;  
    }  

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

class Student implements Cloneable{  
    private int number;  

    private Address addr;  

    public Address getAddr() {  
        return addr;  
    }  

    public void setAddr(Address addr) {  
        this.addr = addr;  
    }  

    public int getNumber() {  
        return number;  
    }  

    public void setNumber(int number) {  
        this.number = number;  
    }  

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

    public static void main(String args[]) {  

        Address addr = new Address();  
        addr.setAdd("杭州市");  
        Student stu1 = new Student();  
        stu1.setNumber(123);  
        stu1.setAddr(addr);  

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

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

        addr.setAdd("西湖區");  

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

結果:
學生1:123,地址:杭州市
學生2:123,地址:杭州市
學生1:123,地址:西湖區
學生2:123,地址:西湖區

怎麼兩個學生的地址都改變了?

原因是淺複製只是複製了addr變數的引用,並沒有真正的開闢另一塊空間,將值複製後再將引用返回給新物件。

為了達到真正的複製物件,而不是純粹引用複製。我們需要將Address類可複製化,並且修改clone方法,完整程式碼如下:

package abc;  

class Address implements Cloneable {  
    private String add;  

    public String getAdd() {  
        return add;  
    }  

    public void setAdd(String add) {  
        this.add = add;  
    }  

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

class Student implements Cloneable{  
    private int number;  

    private Address addr;  

    public Address getAddr() {  
        return addr;  
    }  

    public void setAddr(Address addr) {  
        this.addr = addr;  
    }  

    public int getNumber() {  
        return number;  
    }  

    public void setNumber(int number) {  
        this.number = number;  
    }  

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

    public static void main(String args[]) {  

        Address addr = new Address();  
        addr.setAdd("杭州市");  
        Student stu1 = new Student();  
        stu1.setNumber(123);  
        stu1.setAddr(addr);  

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

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

        addr.setAdd("西湖區");  

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

結果:
學生1:123,地址:杭州市
學生2:123,地址:杭州市
學生1:123,地址:西湖區
學生2:123,地址:杭州市

在深克隆中,無論原型物件的成員變數是值型別還是引用型別,都將複製一份給克隆物件,深克隆將原型物件的所有引用物件也複製一份給克隆物件。

簡單來說,在深克隆中,除了物件本身被複制外,物件所包含的所有成員變數也將複製。
這裡寫圖片描述
在Java語言中,如果需要實現深克隆,可以通過覆蓋Object類的clone()方法實現,也可以通過序列化(Serialization)等方式來實現。

(如果引用型別裡面還包含很多引用型別,或者內層引用型別的類裡面又包含引用型別,使用clone方法就會很麻煩。這時我們可以用序列化的方式來實現物件的深克隆。)

4、工具類BeanUtils和PropertyUtils進行物件複製

Student stu1 = new Student();  
stu1.setNumber(12345);  
Student stu2 = new Student(); 
BeanUtils.copyProperties(stu2,stu1);

這種寫法無論多少種屬性都只需要一行程式碼搞定,很方便吧!除BeanUtils外還有一個名為PropertyUtils的工具類,它也提供copyProperties()方法,作用與BeanUtils的同名方法十分相似,主要的區別在於BeanUtils提供型別轉換功能,即發現兩個JavaBean的同名屬性為不同型別時,在支援的資料類型範圍內進行轉換,而PropertyUtils不支援這個功能,但是速度會更快一些。在實際開發中,BeanUtils使用更普遍一點,犯錯的風險更低一點。

5、通過序列化實現物件的複製

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