1. 程式人生 > >JAVA 物件的clone() 以及深拷貝和淺拷貝

JAVA 物件的clone() 以及深拷貝和淺拷貝

java賦值是複製物件引用,如果我們想要得到一個物件的副本,使用賦

值操作是無法達到目的的:


@Test
public void testassign(){
  Person p1=new Person();
  p1.setAge(31);
  p1.setName("Peter");
 
  Person p2=p1;
  System.out.println(p1==p2);//true
}

如果建立一個物件的新的副本,也就是說他們的初始狀態完全一樣,但以後可以改變各自的狀態,而互不影響,就需要用到java中物件的複製,如原生的clone()方法。

如何進行物件克隆

Object物件有個clone()方法,實現了物件中各個屬性的複製,但它的可見範圍是protected的,所以實體類使用克隆的前提是:

① 實現Cloneable介面,這是一個標記介面,自身沒有方法。 
② 覆蓋clone()方法,可見性提升為public。

@Data
public class Person implements Cloneable {
    private String name;
    private Integer age;
    private Address address;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
 
@Test
public void testShallowCopy() throws Exception{
  Person p1=new Person();
  p1.setAge(31);
  p1.setName("Peter");
 
  Person p2=(Person) p1.clone();
  System.out.println(p1==p2);//false
  p2.setName("Jacky");
  System.out.println("p1="+p1);//p1=Person [name=Peter, age=31]
  System.out.println("p2="+p2);//p2=Person [name=Jacky, age=31]
}

該測試用例只有兩個基本型別的成員,測試達到目的了。

事情貌似沒有這麼簡單,為Person增加一個Address類的成員:

@Data
public class Address {
    private String type;
    private String value;
}

再來測試,問題來了。

@Test
public void testShallowCopy() throws Exception{
  Address address=new Address();
  address.setType("Home");
  address.setValue("北京");
 
  Person p1=new Person();
  p1.setAge(31);
  p1.setName("Peter");
  p1.setAddress(address);
 
  Person p2=(Person) p1.clone();
  System.out.println(p1==p2);//false
 
  p2.getAddress().setType("Office");
  System.out.println("p1="+p1);
  System.out.println("p2="+p2);
}

檢視輸出:

false
p1=Person(name=Peter, age=31, address=Address(type=Office, value=北京))
p2=Person(name=Peter, age=31, address=Address(type=Office, value=北京))

遇到了點麻煩,只修改了p2的地址型別,兩個地址型別都變成了Office。

淺拷貝和深拷貝

前面例項中是淺拷貝和深拷貝的典型用例。

淺拷貝:被複制物件的所有值屬性都含有與原來物件的相同,而所有的物件引用屬性仍然指向原來的物件。

深拷貝:在淺拷貝的基礎上,所有引用其他物件的變數也進行了clone,並指向被複制過的新物件。

也就是說,一個預設的clone()方法實現機制,仍然是賦值。

如果一個被複制的屬性都是基本型別,那麼只需要實現當前類的cloneable機制就可以了,此為淺拷貝。

如果被複制物件的屬性包含其他實體類物件引用,那麼這些實體類物件都需要實現cloneable介面並覆蓋clone()方法。

@Data
public class Address implements Cloneable {
    private String type;
    private String value;
 
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

重新跑前面的測試用例:

false
p1=Person(name=Peter, age=31, address=Address(type=Home, value=北京))
p2=Person(name=Peter, age=31, address=Address(type=Office, value=北京))

clone方式深拷貝小結

① 如果有一個非原生成員,如自定義物件的成員,那麼就需要:

  • 該成員實現Cloneable介面並覆蓋clone()方法,不要忘記提升為public可見。
  • 同時,修改被複制類的clone()方法,增加成員的克隆邏輯。

② 如果被複制物件不是直接繼承Object,中間還有其它繼承層次,每一層super類都需要實現Cloneable介面並覆蓋clone()方法。

與物件成員不同,繼承關係中的clone不需要被複制類的clone()做多餘的工作。

一句話來說,如果實現完整的深拷貝,需要被複制物件的繼承鏈、引用鏈上的每一個物件都實現克隆機制。

前面的例項還可以接受,如果有N個物件成員,有M層繼承關係,就會很麻煩。

利用序列化實現深拷貝

clone機制不是強型別的限制,比如實現了Cloneable並沒有強制繼承鏈上的物件也實現;也沒有強制要求覆蓋clone()方法。因此編碼過程中比較容易忽略其中一個環節,對於複雜的專案排查就是困難了。

要尋找可靠的,簡單的方法,序列化就是一種途徑。

  • 被複制物件的繼承鏈、引用鏈上的每一個物件都實現java.io.Serializable介面。這個比較簡單,不需要實現任何方法,serialVersionID的要求不強制,對深拷貝來說沒毛病。

  • 實現自己的deepClone方法,將this寫入流,再讀出來。俗稱:冷凍-解凍。

@Data
public class Person implements Serializable {
    private String name;
    private Integer age;
    private Address address;
    public Person deepClone() {
        Person p2=null;
        Person p1=this;
        PipedOutputStream out=new PipedOutputStream();
        PipedInputStream in=new PipedInputStream();
        try {
            in.connect(out);
        } catch (IOException e) {
            e.printStackTrace();
        }
 
        try(ObjectOutputStream bo=new ObjectOutputStream(out);
                ObjectInputStream bi=new ObjectInputStream(in);) {
            bo.writeObject(p1);
            p2=(Person) bi.readObject();
 
        } catch (Exception e) {
            e.printStackTrace();
        }
        return p2;
    }
}

原型工廠類

為了便於測試,也節省篇幅,封裝一個工廠類。

公平起見,避免某些工具庫使用快取機制,使用原型方式工廠。

public class PersonFactory{
    public static Person newPrototypeInstance(){
        Address address = new Address();
        address.setType("Home");
        address.setValue("北京");
 
        Person p1 = new Person();
        p1.setAddress(address);
        p1.setAge(31);
        p1.setName("Peter");
        return p1;
    }
}

利用Dozer拷貝物件

Dozer是一個Bean處理類庫。

maven依賴

<dependency>
  <groupId>net.sf.dozer</groupId>
  <artifactId>dozer</artifactId>
  <version>5.5.1</version>
</dependency>

測試用例:

@Data
public class Person {
    private String name;
    private Integer age;
    private Address address;
 
    @Test
    public void testDozer() {
    Person p1=PersonFactory.newPrototypeInstance();
        Mapper mapper = new DozerBeanMapper();
        Person p2 = mapper.map(p1, Person.class);
        p2.getAddress().setType("Office");
        System.out.println("p1=" + p1);
        System.out.println("p2=" + p2);
    }
}
 
@Data
public class Address {
    private String type;
    private String value;
}

輸出:

p1=Person(name=Peter, age=31, address=Address(type=Home, value=北京))
p2=Person(name=Peter, age=31, address=Address(type=Office, value=北京))

注意:在萬次測試中dozer有一個很嚴重的問題,如果DozerBeanMapper物件在for迴圈中建立,效率(dozer:7358)降低近10倍。由於DozerBeanMapper是執行緒安全的,所以不應該每次都建立新的例項。可以自帶的單例工廠DozerBeanMapperSingletonWrapper來建立mapper,或整合到spring中。

還有更暴力的,建立一個People類:

@Data
public class People {
    private String name;
    private String age;//這裡已經不是Integer了
    private Address address;
 
    @Test
    public void testDozer() {
    Person p1=PersonFactory.newPrototypeInstance();
        Mapper mapper = new DozerBeanMapper();
        People p2 = mapper.map(p1, People.class);
        p2.getAddress().setType("Office");
        System.out.println("p1=" + p1);
        System.out.println("p2=" + p2);
    }
}

只要屬性名相同,幹~

繼續蹂躪:

@Data
public class People {
    private String name;
    private String age;
    private Map<String,String> address;//��
 
    @Test
    public void testDozer() {
    Person p1=PersonFactory.newPrototypeInstance();
        Mapper mapper = new DozerBeanMapper();
        People p2 = mapper.map(p1, People.class);
        p2.getAddress().put("type", "Office");
        System.out.println("p1=" + p1);
        System.out.println("p2=" + p2);
    }
}

利用Commons-BeanUtils複製物件

maven依賴

<dependency>
  <groupId>commons-beanutils</groupId>
  <artifactId>commons-beanutils</artifactId>
  <version>1.9.3</version>
</dependency>

測試用例:

@Data
public class Person {
    private String name;
    private String age;
    private Address address;
 
    @Test
    public void testCommonsBeanUtils(){
    Person p1=PersonFactory.newPrototypeInstance();
        try {
            Person p2=(Person) BeanUtils.cloneBean(p1);
            System.out.println("p1=" + p1);
            p2.getAddress().setType("Office");
            System.out.println("p2=" + p2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

利用cglib複製物件

maven依賴:

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.2.4</version>
</dependency>

測試用例:

@Test
public void testCglib(){
  Person p1=PersonFactory.newPrototypeInstance();
  BeanCopier beanCopier=BeanCopier.create(Person.class, Person.class, false);
  Person p2=new Person();
  beanCopier.copy(p1, p2,null);
  p2.getAddress().setType("Office");
  System.out.println("p1=" + p1);
  System.out.println("p2=" + p2);
}

結果大跌眼鏡,cglib這麼牛x,居然是淺拷貝。不過cglib提供了擴充套件能力:

@Test
public void testCglib(){
  Person p1=PersonFactory.newPrototypeInstance();
  BeanCopier beanCopier=BeanCopier.create(Person.class, Person.class, true);
  Person p2=new Person();
  beanCopier.copy(p1, p2, new Converter(){
    @Override
    public Object convert(Object value, Class target, Object context) {
      if(target.isSynthetic()){
        BeanCopier.create(target, target, true).copy(value, value, this);
      }
      return value;
    }
  });
  p2.getAddress().setType("Office");
  System.out.println("p1=" + p1);
  System.out.println("p2=" + p2);
}

Orika複製物件

orika的作用不僅僅在於處理bean拷貝,更擅長各種型別之間的轉換。

maven依賴:

<dependency>
  <groupId>ma.glasnost.orika</groupId>
  <artifactId>orika-core</artifactId>
  <version>1.5.0</version>
</dependency>
</dependencies>

測試用例:

@Test
public void testOrika() {
  MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
 
  mapperFactory.classMap(Person.class, Person.class)
  .byDefault()
  .register();
  ConverterFactory converterFactory = mapperFactory.getConverterFactory();
  MapperFacade mapper = mapperFactory.getMapperFacade();
 
  Person p1=PersonFactory.newPrototypeInstance();
  Person p2 = mapper.map(p1, Person.class);
  System.out.println("p1=" + p1);
  p2.getAddress().setType("Office");
  System.out.println("p2=" + p2);
}

Spring BeanUtils複製物件

給Spring個面子,貌似它不支援深拷貝。

Person p1=PersonFactory.newPrototypeInstance();
Person p2 = new Person();
Person p2 = (Person) BeanUtils.cloneBean(p1);
//BeanUtils.copyProperties(p2, p1);//這個更沒戲

深拷貝效能對比

@Test
public void testBatchDozer(){
  Long start=System.currentTimeMillis();
  Mapper mapper = new DozerBeanMapper();
  for(int i=0;i<10000;i++){
    Person p1=PersonFactory.newPrototypeInstance();
    Person p2 = mapper.map(p1, Person.class);
  }
  System.out.println("dozer:"+(System.currentTimeMillis()-start));
  //dozer:721
}
@Test
public void testBatchBeanUtils(){
  Long start=System.currentTimeMillis();
  for(int i=0;i<10000;i++){
    Person p1=PersonFactory.newPrototypeInstance();
    try {
      Person p2=(Person) BeanUtils.cloneBean(p1);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
  System.out.println("commons-beanutils:"+(System.currentTimeMillis()-start));
  //commons-beanutils:229
}
@Test
public void testBatchCglib(){
  Long start=System.currentTimeMillis();
  for(int i=0;i<10000;i++){
    Person p1=PersonFactory.newPrototypeInstance();
    BeanCopier beanCopier=BeanCopier.create(Person.class, Person.class, true);
    Person p2=new Person();
    beanCopier.copy(p1, p2, new Converter(){
      @Override
      public Object convert(Object value, Class target, Object context) {
        if(target.isSynthetic()){
          BeanCopier.create(target, target, true).copy(value, value, this);
        }
        return value;
      }
    });
  }
  System.out.println("cglib:"+(System.currentTimeMillis()-start));
  //cglib:133
}
@Test
public void testBatchSerial(){
  Long start=System.currentTimeMillis();
  for(int i=0;i<10000;i++){
    Person p1=PersonFactory.newPrototypeInstance();
    Person p2=p1.deepClone();
  }
  System.out.println("serializable:"+(System.currentTimeMillis()-start));
  //serializable:687
}
@Test
public void testBatchOrika() {
  MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
 
  mapperFactory.classMap(Person.class, Person.class)
  .field("name", "name")
  .byDefault()
  .register();
  ConverterFactory converterFactory = mapperFactory.getConverterFactory();
  MapperFacade mapper = mapperFactory.getMapperFacade();
 
  Long start=System.currentTimeMillis();
  for(int i=0;i<10000;i++){
    Person p1=PersonFactory.newPrototypeInstance();
    Person p2 = mapper.map(p1, Person.class);
  }
  System.out.println("orika:"+(System.currentTimeMillis()-start));
  //orika:83
}
 
@Test
public void testBatchClone(){
  Long start=System.currentTimeMillis();
  for(int i=0;i<10000;i++){
    Person p1=PersonFactory.newPrototypeInstance();
    try {
      Person p2=(Person) p1.clone();
    } catch (CloneNotSupportedException e) {
      e.printStackTrace();
    }
  }
  System.out.println("clone:"+(System.currentTimeMillis()-start));
  //clone:8
}

(10k)效能比較:

//dozer:721
//commons-beanutils:229
//cglib:133
//serializable:687
//orika:83
//clone:8

深拷貝總結

原生的clone效率無疑是最高的,用腳趾頭都能想到。

偶爾用一次,用哪個都問題都不大。

一般效能要求稍高的應用場景,cglib和orika完全可以接受。

另外一個考慮的因素,如果專案已經引入了某個依賴,就用那個依賴來做吧,沒必要再引入一個第三方依賴。

另一個版本:

1.什麼是"克隆"?

在實際程式設計過程中,我們常常要遇到這種情況:有一個物件A,在某一時刻A中已經包含了一些有效值,此時可能 會需要一個和A完全相同新物件B,並且此後對B任何改動都不會影響到A中的值,也就是說,A與B是兩個獨立的物件,但B的初始值是由A物件確定的。在 Java語言中,用簡單的賦值語句是不能滿足這種需求的。要滿足這種需求雖然有很多途徑,但實現clone()方法是其中最簡單,也是最高效的手段。 
Java的所有類都預設繼承java.lang.Object類,在java.lang.Object類中有一個方法clone()。JDK API的說明文件解釋這個方法將返回Object物件的一個拷貝。要說明的有兩點:一是拷貝物件返回的是一個新物件,而不是一個引用。二是拷貝物件與用 new操作符返回的新物件的區別就是這個拷貝已經包含了一些原來物件的資訊,而不是物件的初始資訊。 

2.怎樣應用clone方法

一個很典型的呼叫clone()程式碼如下: 

class CloneClass implements Cloneable{ 
 public int aInt; 
 public Object clone(){ 
  CloneClass o = null; 
  try{ 
   o = (CloneClass)super.clone(); 
  }catch(CloneNotSupportedException e){ 
   e.printStackTrace(); 
  } 
  return o; 
 } 
}

有三個值得注意的地方,一是希望能實現clone功能的CloneClass類實現了Cloneable介面,這個介面屬於java.lang 包,java.lang包已經被預設的匯入類中,所以不需要寫成java.lang.Cloneable。另一個值得請注意的是過載了clone()方 法。最後在clone()方法中呼叫了super.clone(),這也意味著無論clone類的繼承結構是什麼樣的,super.clone()直接或 間接呼叫了java.lang.Object類的clone()方法。下面再詳細的解釋一下這幾點。 

應該說第三點是最重要的,仔細 觀察一下Object類的clone()一個native方法,native方法的效率一般來說都是遠高於java中的非native方法。這也解釋了為 什麼要用Object中clone()方法而不是先new一個類,然後把原始物件中的資訊賦到新物件中,雖然這也實現了clone功能。對於第二點,也要 觀察Object類中的clone()還是一個protected屬性的方法。這也意味著如果要應用clone()方法,必須繼承Object類,在 Java中所有的類是預設繼承Object類的,也就不用關心這點了。然後重寫clone()方法。還有一點要考慮的是為了讓其它類能呼叫這個clone 類的clone()方法,重寫之後要把clone()方法的屬性設定為public。 

那麼clone類為什麼還要實現 Cloneable介面呢?稍微注意一下,Cloneable介面是不包含任何方法的!其實這個介面僅僅是一個標誌,而且這個標誌也僅僅是針對 Object類中clone()方法的,如果clone類沒有實現Cloneable介面,並呼叫了Object的clone()方法(也就是呼叫了 super.Clone()方法),那麼Object的clone()方法就會丟擲CloneNotSupportedException異常。 

3.深拷貝與淺拷貝

淺拷貝是指拷貝物件時僅僅拷貝物件本身(包括物件中的基本變數),而不拷貝物件包含的引用指向的物件。深拷貝不僅拷貝物件本身,而且拷貝物件包含的引用指向的所有物件。舉例來說更加清楚:物件A1中包含對B1的引用,B1中包含對C1的引用。淺拷貝A1得到A2,A2 中依然包含對B1的引用,B1中依然包含對C1的引用。深拷貝則是對淺拷貝的遞迴,深拷貝A1得到A2,A2中包含對B2(B1的copy)的引用,B2 中包含對C2(C1的copy)的引用。

若不對clone()方法進行改寫,則呼叫此方法得到的物件即為淺拷貝,下面我們著重談一下深拷貝。執行下面的程式,看一看淺拷貝:

class Professor0 implements Cloneable {
    String name;
    int age;
 
    Professor0(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
class Student0 implements Cloneable {
    String name;// 常量物件。
    int age;
    Professor0 p;// 學生1和學生2的引用值都是一樣的。
 
    Student0(String name, int age, Professor0 p) {
        this.name = name;
        this.age = age;
        this.p = p;
    }
 
    public Object clone() {
        Student0 o = null;
        try {
            o = (Student0) super.clone();
        } catch (CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
 
        return o;
    }
}
public class ShallowCopy {
    public static void main(String[] args) {
        Professor0 p = new Professor0("wangwu", 50);
        Student0 s1 = new Student0("zhangsan", 18, p);
        Student0 s2 = (Student0) s1.clone();
        s2.p.name = "lisi";
        s2.p.age = 30;
        s2.name = "z";
        s2.age = 45;
        System.out.println("學生s1的姓名:" + s1.name + "\n學生s1教授的姓名:" + s1.p.name + "," + "\n學生s1教授的年紀" + s1.p.age);// 學生1的教授
    }
}

執行結果:

s2變了,但s1也變了,證明s1的p和s2的p指向的是同一個物件。這在我們有的實際需求中,卻不是這樣,因而我們需要深拷貝:

class Professor implements Cloneable {
    String name;
    int age;
 
    Professor(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    public Object clone() {
        Object o = null;
        try {
            o = super.clone();
        } catch (CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
        return o;
    }
}
class Student implements Cloneable {
    String name;
    int age;
    Professor p;
 
    Student(String name, int age, Professor p) {
        this.name = name;
        this.age = age;
        this.p = p;
    }
 
    public Object clone() {
        Student o = null;
        try {
            o = (Student) super.clone();
        } catch (CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
        o.p = (Professor) p.clone();
        return o;
    }
}
public class DeepCopy {
    public static void main(String args[]) {
        long t1 = System.currentTimeMillis();
        Professor p = new Professor("wangwu", 50);
        Student s1 = new Student("zhangsan", 18, p);
        Student s2 = (Student) s1.clone();
        s2.p.name = "lisi";
        s2.p.age = 30;
        System.out.println("name=" + s1.p.name + "," + "age=" + s1.p.age);// 學生1的教授不改變。
        long t2 = System.currentTimeMillis();
        System.out.println(t2-t1);
    }
}

執行結果:

當然我們還有一種深拷貝方法,就是將物件序列化:

class Professor2 implements Serializable {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    String name;
    int age;
 
    Professor2(String name, int age) {
        this.name = name;
        this.age = age;
    }
class Student2 implements Serializable {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    String name;// 常量物件。
    int age;
    Professor2 p;// 學生1和學生2的引用值都是一樣的。
 
    Student2(String name, int age, Professor2 p) {
        this.name = name;
        this.age = age;
        this.p = p;
    }
 
    public Object deepClone() throws IOException, OptionalDataException,
            ClassNotFoundException {
        // 將物件寫到流裡
        ByteArrayOutputStream bo = new ByteArrayOutputStream();
        ObjectOutputStream oo = new ObjectOutputStream(bo);
        oo.writeObject(this);
        // 從流裡讀出來
        ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
        ObjectInputStream oi = new ObjectInputStream(bi);
        return (oi.readObject());
    }
 
}
public class DeepCopy2 {
     
    /**
     * @param args
     */
    public static void main(String[] args) throws OptionalDataException,
            IOException, ClassNotFoundException {
        long t1 = System.currentTimeMillis();
        Professor2 p = new Professor2("wangwu", 50);
        Student2 s1 = new Student2("zhangsan", 18, p);
        Student2 s2 = (Student2) s1.deepClone();
        s2.p.name = "lisi";
        s2.p.age = 30;
        System.out.println("name=" + s1.p.name + "," + "age=" + s1.p.age); // 學生1的教授不改變。
        long t2 = System.currentTimeMillis();
        System.out.println(t2-t1);
    }
 
}

要想序列化物件,必須先建立一個OutputStream,然後把它嵌入ObjectOutputStream。這時就能用writeObject()方法把物件寫入OutputStream。讀的時候需要把InputStream嵌到ObjectInputStream中,然後再呼叫readObject()方法。不過這樣讀出來的只是一個Object的reference,因此,在用之前,還要下轉型。

物件序列化不僅能儲存物件的副本,而且會跟著物件中的reference把它所引用的物件也儲存起來,然後再繼續跟蹤那些物件的reference,以此類推。這種情形常被稱作”單個物件所聯結的‘物件網’ “。

但是序列化卻很耗時,在一些框架中,我們便可以感受到,它們往往將物件進行序列化後進行傳遞,耗時較多。

請各位大佬多多包含