Java的深拷贝和浅拷贝

  关于Java的深拷贝和浅拷贝,简单来讲就是建立一个和已知对象如出一辙的对象。可能平常编码过程当中用的很少,可是这是一个面试常常会问的问题,并且了解深拷贝和浅拷贝的原理,对于Java中的所谓值传递或者引用传递将会有更深的理解。html

一、建立对象的5种方式

  ①、经过 new 关键字java

  这是最经常使用的一种方式,经过 new 关键字调用类的有参或无参构造方法来建立对象。好比 Object obj = new Object();面试

  ②、经过 Class 类的 newInstance() 方法数组

  这种默认是调用类的无参构造方法建立对象。好比 Person p2 = (Person) Class.forName("com.ys.test.Person").newInstance();网络

  ③、经过 Constructor 类的 newInstance 方法ide

  这和第二种方法类时,都是经过反射来实现。经过 java.lang.relect.Constructor 类的 newInstance() 方法指定某个构造器来建立对象。测试

  Person p3 = (Person) Person.class.getConstructors()[0].newInstance();this

  实际上第二种方法利用 Class 的 newInstance() 方法建立对象,其内部调用仍是 Constructor 的 newInstance() 方法。编码

  ④、利用 Clone 方法spa

  Clone 是 Object 类中的一个方法,经过 对象A.clone() 方法会建立一个内容和对象 A 如出一辙的对象 B,clone 克隆,顾名思义就是建立一个如出一辙的对象出来。

  Person p4 = (Person) p3.clone();

  ⑤、反序列化

  序列化是把堆内存中的 Java 对象数据,经过某种方式把对象存储到磁盘文件中或者传递给其余网络节点(在网络上传输)。而反序列化则是把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。

  具体如何实现能够参考我 这篇博文

三、Clone 方法

  本篇博客咱们讲解的是 Java 的深拷贝和浅拷贝,其实现方式正是经过调用 Object 类的 clone() 方法来完成。在 Object.class 类中,源码为:

protected native Object clone() throws CloneNotSupportedException;

  这是一个用 native 关键字修饰的方法,关于native关键字有一篇博客专门有介绍,不理解也不要紧,只须要知道用 native 修饰的方法就是告诉操做系统,这个方法我不实现了,让操做系统去实现。具体怎么实现咱们不须要了解,只须要知道 clone方法的做用就是复制对象,产生一个新的对象。那么这个新的对象和原对象是什么关系呢?

四、基本类型和引用类型

  这里再给你们普及一个概念,在 Java 中基本类型和引用类型的区别。

  在 Java 中数据类型能够分为两大类:基本类型和引用类型。

  基本类型也称为值类型,分别是字符类型 char,布尔类型 boolean以及数值类型 byte、short、int、long、float、double。

  引用类型则包括类、接口、数组、枚举等。

  Java 将内存空间分为堆和栈。基本类型直接在栈中存储数值,而引用类型是将引用放在栈中,实际存储的值是放在堆中,经过栈中的引用指向堆中存放的数据。

  

  上图定义的 a 和 b 都是基本类型,其值是直接存放在栈中的;而 c 和 d 是 String 声明的,这是一个引用类型,引用地址是存放在 栈中,而后指向堆的内存空间。

  下面 d = c;这条语句表示将 c 的引用赋值给 d,那么 c 和 d 将指向同一块堆内存空间。

五、浅拷贝

  咱们看以下这段代码:

package com.ys.test;

public class Person implements Cloneable{
    public String pname;
    public int page;
    public Address address;
    public Person() {}
    
    public Person(String pname,int page){
        this.pname = pname;
        this.page = page;
        this.address = new Address();
    }
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    
    public void setAddress(String provices,String city ){
        address.setAddress(provices, city);
    }
    public void display(String name){
        System.out.println(name+":"+"pname=" + pname + ", page=" + page +","+ address);
    }

    public String getPname() {
        return pname;
    }

    public void setPname(String pname) {
        this.pname = pname;
    }

    public int getPage() {
        return page;
    }

    public void setPage(int page) {
        this.page = page;
    }
    
}
View Code
package com.ys.test;

public class Address {
    private String provices;
    private String city;
    public void setAddress(String provices,String city){
        this.provices = provices;
        this.city = city;
    }
    @Override
    public String toString() {
        return "Address [provices=" + provices + ", city=" + city + "]";
    }
    
}
View Code

  这是一个咱们要进行赋值的原始类 Person。下面咱们产生一个 Person 对象,并调用其 clone 方法复制一个新的对象。

  注意:调用对象的 clone 方法,必需要让类实现 Cloneable 接口,而且覆写 clone 方法。

  测试:

@Test
public void testShallowClone() throws Exception{
    Person p1 = new Person("zhangsan",21);
    p1.setAddress("湖北省", "武汉市");
    Person p2 = (Person) p1.clone();
    System.out.println("p1:"+p1);
    System.out.println("p1.getPname:"+p1.getPname().hashCode());
    
    System.out.println("p2:"+p2);
    System.out.println("p2.getPname:"+p2.getPname().hashCode());
    
    p1.display("p1");
    p2.display("p2");
    p2.setAddress("湖北省", "荆州市");
    System.out.println("将复制以后的对象地址修改:");
    p1.display("p1");
    p2.display("p2");
}
View Code

  打印结果为:

  

  首先看原始类 Person 实现 Cloneable 接口,而且覆写 clone 方法,它还有三个属性,一个引用类型 String定义的 pname,一个基本类型 int定义的 page,还有一个引用类型 Address ,这是一个自定义类,这个类也包含两个属性 pprovices 和 city 。

  接着看测试内容,首先咱们建立一个Person 类的对象 p1,其pname 为zhangsan,page为21,地址类 Address 两个属性为 湖北省和武汉市。接着咱们调用 clone() 方法复制另外一个对象 p2,接着打印这两个对象的内容。

  从第 1 行和第 3 行打印结果:

  p1:com.ys.test.Person@349319f9

  p2:com.ys.test.Person@258e4566

  能够看出这是两个不一样的对象。

  从第 5 行和第 6 行打印的对象内容看,原对象 p1 和克隆出来的对象 p2 内容彻底相同。

  代码中咱们只是更改了克隆对象 p2 的属性 Address 为湖北省荆州市(原对象 p1 是湖北省武汉市) ,可是从第 7 行和第 8 行打印结果来看,原对象 p1 和克隆对象 p2 的 Address 属性都被修改了。

  也就是说对象 Person 的属性 Address,通过 clone 以后,其实只是复制了其引用,他们指向的仍是同一块堆内存空间,当修改其中一个对象的属性 Address,另外一个也会跟着变化。

  

 

  浅拷贝:建立一个新对象,而后将当前对象的非静态字段复制到该新对象,若是字段是值类型的,那么对该字段执行复制;若是该字段是引用类型的话,则复制引用但不复制引用的对象。所以,原始对象及其副本引用同一个对象。

六、深拷贝

  弄清楚了浅拷贝,那么深拷贝就很容易理解了。

  深拷贝:建立一个新对象,而后将当前对象的非静态字段复制到该新对象,不管该字段是值类型的仍是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另外一个对象的内容。

  

  那么该如何实现深拷贝呢?Object 类提供的 clone 是只能实现 浅拷贝的。

七、如何实现深拷贝?

  深拷贝的原理咱们知道了,就是要让原始对象和克隆以后的对象所具备的引用类型属性不是指向同一块堆内存,这里有三种实现思路。

  ①、让每一个引用类型属性内部都重写clone() 方法

  既然引用类型不能实现深拷贝,那么咱们将每一个引用类型都拆分为基本类型,分别进行浅拷贝。好比上面的例子,Person 类有一个引用类型 Address(其实String 也是引用类型,可是String类型有点特殊,后面会详细讲解),咱们在 Address 类内部也重写 clone 方法。以下:

  Address.class:

 1 package com.ys.test;
 2 
 3 public class Address implements Cloneable{
 4     private String provices;
 5     private String city;
 6     public void setAddress(String provices,String city){
 7         this.provices = provices;
 8         this.city = city;
 9     }
10     @Override
11     public String toString() {
12         return "Address [provices=" + provices + ", city=" + city + "]";
13     }
14     @Override
15     protected Object clone() throws CloneNotSupportedException {
16         return super.clone();
17     }
18     
19 }
View Code

  Person.class 的 clone() 方法:

1     @Override
2     protected Object clone() throws CloneNotSupportedException {
3         Person p = (Person) super.clone();
4         p.address = (Address) address.clone();
5         return p;
6     }
View Code

  测试仍是和上面同样,咱们会发现更改了p2对象的Address属性,p1 对象的 Address 属性并无变化。

  可是这种作法有个弊端,这里咱们Person 类只有一个 Address 引用类型,而 Address 类没有,因此咱们只用重写 Address 类的clone 方法,可是若是 Address 类也存在一个引用类型,那么咱们也要重写其clone 方法,这样下去,有多少个引用类型,咱们就要重写多少次,若是存在不少引用类型,那么代码量显然会很大,因此这种方法不太合适。

  ②、利用序列化

  序列化是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。这里写到流中的对象则是原始对象的一个拷贝,由于原始对象还存在 JVM 中,因此咱们能够利用对象的序列化产生克隆对象,而后经过反序列化获取这个对象。

  注意每一个须要序列化的类都要实现 Serializable 接口,若是有某个属性不须要序列化,能够将其声明为 transient,即将其排除在克隆属性以外。

//深度拷贝
public Object deepClone() throws Exception{
    // 序列化
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);

    oos.writeObject(this);

    // 反序列化
    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bis);

    return ois.readObject();
}
View Code

   由于序列化产生的是两个彻底独立的对象,全部不管嵌套多少个引用类型,序列化都是能实现深拷贝的。

相关文章
相关标签/搜索