浅谈Java中的浅拷贝和深拷贝

写在前面的


前几天在复习这块内容的时候看到了几篇不错的博客,文章读起来通俗易懂,今天打算把它们整合一下记录下来。(注:原文连接在此文章的底部)java

假如你想复制一个简单变量,很简单:bash

int apples = 5;
int pears = apples; 
复制代码

不只仅是int类型,其它七种基本数据类型(byte,short,long,double,float,char,boolean)一样适用于该类状况。app

可是若是你想复制一个对象,那状况就稍有些复杂了,可能你会这么写:socket

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; 
         
        System.out.println("学生1:" + stu1.getNumber()); 
        System.out.println("学生2:" + stu2.getNumber()); 
    } 
} 
复制代码

打印结果:ide

学生1:12345 
学生2:12345 
复制代码

这里咱们定义了一个Student类,该类只有一个number字段。而后咱们new了一个Student实例,而后将该值赋值给stu2实例(Student stu2 = stu1;)。再看打印结果,做为一个新手,感受对象复制不过如此,难道真的就是这样的吗?接下来咱们试着改变stu2实例的number字段,再打印结果看看:函数

stu2.setNumber(54321); 
 
System.out.println("学生1:" + stu1.getNumber()); 
System.out.println("学生2:" + stu2.getNumber()); 
复制代码

打印结果:布局

学生1:54321 
学生2:54321 
复制代码

这就奇怪了,为何改变stu2number值,stu1number值也发生了改变呢?post

缘由出在stu2 = stu1这一句,该句的做用是将stu1的引用赋值给stu2,这样stu1stu2指向内存堆中的同一个对象。如图: 性能

那么,怎么能达到复制一个对象呢,对于“复制对象”,咱们应该有什么要特别注意的地方呢?接下来就引出今天讨论的主角: 对象拷贝。

如今才是概述


  对象拷贝(Object Copy)就是将一个对象的属性拷贝到另外一个有着相同类类型的对象中去。在程序中拷贝对象是很常见的,主要是为了在新的上下文环境中复用对象的部分或所有数据。Java中有三种类型的对象拷贝:浅拷贝(Shallow Copy)、深拷贝(Deep Copy)、延迟拷贝(Lazy Copy)。ui

1. 浅拷贝

  • 什么是浅拷贝?

  浅拷贝是按位拷贝对象,它会建立一个新对象,这个对象有着原始对象属性值的一份精确拷贝。若是属性是基本类型,拷贝的就是基本类型的值;若是属性是内存地址(引用类型),拷贝的就是内存地址 ,所以若是其中一个对象改变了这个地址,就会影响到另外一个对象。

在上图中, SourceObject有一个 int类型的属性 " field1"和一个引用类型属性" refObj"(引用 ContainedObject类型的对象)。当对 SourceObject作浅拷贝时,建立了 CopiedObject,它有一个包含" field1"拷贝值的属性" field2"以及仍指向 refObj自己的引用。因为" field1"是基本类型,因此只是将它的值拷贝给" field2",可是因为" refObj"是一个引用类型, 因此 CopiedObject指向" refObj"相同的地址。所以对 SourceObject中的" refObj"所作的任何改变都会影响到 CopiedObject

  • 如何实现浅拷贝

是否记得万类之王Object。它有9个方法(getClass(), hashCode(), equals(), clone(), toString(), notify(), notifyAll(), wait(), finalize()),其中一个为clone()方法。 该方法的签名是:

protected native Object clone() throws CloneNotSupportedException;
复制代码

由于每一个类直接或间接的父类都是Object,所以它们都含有clone()方法。 要想对一个对象进行复制,就须要对clone()方法覆盖。

关于clone

clone顾名思义就是复制, 在Java语言中, clone方法被对象调用,因此会复制对象。所谓的复制对象,首先要分配一个和源对象一样大小的空间,在这个空间中建立一个新的对象。那么在java语言中,有几种方式能够建立对象呢?

1. 使用new操做符建立一个对象

2. 使用clone方法复制一个对象

  那么这两种方式有什么相同和不一样呢? new操做符的本意是分配内存。程序执行到new操做符时, 首先去看new操做符后面的类型,由于知道了类型,才能知道要分配多大的内存空间。分配完内存以后,再调用构造函数,填充对象的各个域,这一步叫作对象的初始化,构造方法返回后,一个对象建立完毕,能够把他的引用(地址)发布到外部,在外部就可使用这个引用操纵这个对象。(具体细节可参考个人另外一篇文章《JVM之对象的建立、内存布局、访问总结》

  而clone在第一步是和new类似的, 都是分配内存,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,而后再使用原对象中对应的各个域,填充新对象的域, 填充完成以后,clone方法返回,一个新的相同的对象被建立,一样能够把这个新对象的引用发布到外部。

下面是浅拷贝的一个例子

public class Subject {

   private String name; 

   public Subject(String s) { 
      name = s; 
   } 

   public String getName() { 
      return name; 
   } 

   public void setName(String s) { 
      name = s; 
   } 
}
复制代码
public class Student implements Cloneable { 

   // 对象引用 
   private Subject subj; 

   private String name; 

   public Student(String s, String sub) { 
      name = s; 
      subj = new Subject(sub); 
   } 

   public Subject getSubj() { 
      return subj; 
   } 

   public String getName() { 
      return name; 
   } 

   public void setName(String s) { 
      name = s; 
   } 

   /** 
    *  重写clone()方法 
    * @return 
    */ 
   public Object clone() { 
      //浅拷贝 
      try { 
         // 直接调用父类的clone()方法
         return super.clone(); 
      } catch (CloneNotSupportedException e) { 
         return null; 
      } 
   } 
}
复制代码
public class CopyTest {

    public static void main(String[] args) {
        // 原始对象
        Student stud = new Student("John", "Algebra");
        System.out.println("Original Object: " + stud.getName() + " - " + stud.getSubj().getName());

        // 拷贝对象
        Student clonedStud = (Student) stud.clone();
        System.out.println("Cloned Object: " + clonedStud.getName() + " - " + clonedStud.getSubj().getName());

        // 原始对象和拷贝对象是否同样:
        System.out.println("Is Original Object the same with Cloned Object: " + (stud == clonedStud));
        // 原始对象和拷贝对象的name属性是否同样
        System.out.println("Is Original Object's field name the same with Cloned Object: " + (stud.getName() == clonedStud.getName()));
        // 原始对象和拷贝对象的subj属性是否同样
        System.out.println("Is Original Object's field subj the same with Cloned Object: " + (stud.getSubj() == clonedStud.getSubj()));

        stud.setName("Dan");
        stud.getSubj().setName("Physics");

        System.out.println("Original Object after it is updated: " + stud.getName() + " - " + stud.getSubj().getName());
        System.out.println("Cloned Object after updating original object: " + clonedStud.getName() + " - " + clonedStud.getSubj().getName());
    }
}

复制代码

​ 输出结果以下:

Original Object: John - Algebra
Cloned Object: John - Algebra
Is Original Object the same with Cloned Object: false
Is Original Object's field name the same with Cloned Object: true Is Original Object's field subj the same with Cloned Object: true
Original Object after it is updated: Dan - Physics
Cloned Object after updating original object: John - Physics
复制代码

在这个例子中,我让要拷贝的类Student实现了Clonable接口并重写Object类的clone()方法,而后在方法内部调用super.clone()方法。从输出结果中咱们能够看到,对原始对象stud"name"属性所作的改变并无影响到拷贝对象clonedStud,可是对引用对象subj"name"属性所作的改变影响到了拷贝对象clonedStud

2. 深拷贝

  • 什么是深拷贝

  深拷贝会拷贝全部的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一块儿拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢而且花销较大。

在上图中, SourceObject有一个int类型的属性 " field1"和一个引用类型属性" refObj1"(引用ContainedObject类型的对象)。当对 SourceObject作深拷贝时,建立了 CopiedObject,它有一个包含" field1"拷贝值的属性" field2"以及包含" refObj1"拷贝值的引用类型属性" refObj2" 。所以对 SourceObject中的" refObj"所作的任何改变都不会影响到 CopiedObject

  • 如何实现深拷贝

下面是实现深拷贝的一个例子。只是在浅拷贝的例子上作了一点小改动,SubjectCopyTest 类都没有变化。

public class Student implements Cloneable { 
   // 对象引用 
   private Subject subj; 

   private String name; 

   public Student(String s, String sub) { 
      name = s; 
      subj = new Subject(sub); 
   } 

   public Subject getSubj() { 
      return subj; 
   } 

   public String getName() { 
      return name; 
   } 

   public void setName(String s) { 
      name = s; 
   } 

   /** 
    * 重写clone()方法 
    * 
    * @return 
    */ 
   public Object clone() { 
      // 深拷贝,建立拷贝类的一个新对象,这样就和原始对象相互独立
      Student s = new Student(name, subj.getName()); 
      return s; 
   } 
}
复制代码

输出结果以下:

Original Object: John - Algebra
Cloned Object: John - Algebra
Is Original Object the same with Cloned Object: false
Is Original Object's field name the same with Cloned Object: true Is Original Object's field subj the same with Cloned Object: false
Original Object after it is updated: Dan - Physics
Cloned Object after updating original object: John - Algebra
复制代码

很容易发现clone()方法中的一点变化。由于它是深拷贝,因此你须要建立拷贝类的一个对象。由于在Student类中有对象引用,因此须要在Student类中实现Cloneable接口而且重写clone方法。

3. 经过序列化实现深拷贝

也能够经过序列化来实现深拷贝。序列化是干什么的?它将整个对象图写入到一个持久化存储文件中而且当须要的时候把它读取回来, 这意味着当你须要把它读取回来时你须要整个对象图的一个拷贝。这就是当你深拷贝一个对象时真正须要的东西。请注意,当你经过序列化进行深拷贝时,必须确保对象图中全部类都是可序列化的。

public class ColoredCircle implements Serializable { 

   private int x; 
   private int y; 

   public ColoredCircle(int x, int y) { 
      this.x = x; 
      this.y = y; 
   } 

   public int getX() { 
      return x; 
   } 

   public void setX(int x) { 
      this.x = x; 
   } 

   public int getY() { 
      return y; 
   } 

   public void setY(int y) { 
      this.y = y; 
   } 

   @Override 
   public String toString() { 
      return "x=" + x + ", y=" + y; 
   } 
}
复制代码
public class DeepCopy {

   public static void main(String[] args) throws IOException { 
      ObjectOutputStream oos = null; 
      ObjectInputStream ois = null; 

      try { 
         // 建立原始的可序列化对象 
         ColoredCircle c1 = new ColoredCircle(100, 100); 
         System.out.println("Original = " + c1); 

         ColoredCircle c2 = null; 

         // 经过序列化实现深拷贝 
         ByteArrayOutputStream bos = new ByteArrayOutputStream(); 
         oos = new ObjectOutputStream(bos); 
         // 序列化以及传递这个对象 
         oos.writeObject(c1); 
         oos.flush(); 
         ByteArrayInputStream bin = new        ByteArrayInputStream(bos.toByteArray()); 
         ois = new ObjectInputStream(bin); 
         // 返回新的对象 
         c2 = (ColoredCircle) ois.readObject(); 

         // 校验内容是否相同 
         System.out.println("Copied = " + c2); 
         // 改变原始对象的内容 
         c1.setX(200); 
         c1.setY(200); 
         // 查看每个如今的内容 
         System.out.println("Original = " + c1); 
         System.out.println("Copied = " + c2); 
      } catch (Exception e) { 
         System.out.println("Exception in main = " + e); 
      } finally { 
         oos.close(); 
         ois.close(); 
      } 
   } 
}

复制代码

​ 输出结果以下:

Original = x=100, y=100
Copied   = x=100, y=100
Original = x=200, y=200
Copied   = x=100, y=100
复制代码

这里,你只须要作如下几件事儿:

  • 确保对象图中的全部类都是可序列化的
  • 建立输入输出流
  • 使用这个输入输出流来建立对象输入和对象输出流
  • 将你想要拷贝的对象传递给对象输出流
  • 从对象输入流中读取新的对象而且转换回你所发送的对象的类

在这个例子中,我建立了一个ColoredCircle对象c1而后将它序列化(将它写到ByteArrayOutputStream中). 而后我反序列化这个序列化后的对象并将它保存到c2中。随后我修改了原始对象c1。而后结果如你所见,c1不一样于c2,对c1所作的任何修改都不会影响c2。

注意: 序列化这种方式有其自身的限制和问题:

  1. 由于没法序列化transient变量, 使用这种方法将没法拷贝transient变量。

  2. 再就是性能问题。建立一个socket, 序列化一个对象, 经过socket传输它,而后反序列化它,这个过程与调用已有对象的方法相比是很慢的。因此在性能上会有天壤之别。若是性能对你的代码来讲是相当重要的,建议不要使用这种方式。它比经过实现Clonable接口这种方式来进行深拷贝几乎多花100倍的时间。

4. 延迟拷贝

  延迟拷贝是浅拷贝和深拷贝的一个组合,实际上不多会使用。 当最开始拷贝一个对象时,会使用速度较快的浅拷贝,还会使用一个计数器来记录有多少对象共享这个数据。当程序想要修改原始的对象时,它会决定数据是否被共享(经过检查计数器)并根据须要进行深拷贝。

  延迟拷贝从外面看起来就是深拷贝,可是只要有可能它就会利用浅拷贝的速度。当原始对象中的引用不常常改变的时候可使用延迟拷贝。因为存在计数器,效率降低很高,但只是常量级的开销。并且, 在某些状况下, 循环引用会致使一些问题。

5. 如何选择

  若是对象的属性全是基本类型的,那么可使用浅拷贝,可是若是对象有引用属性,那就要基于具体的需求来选择浅拷贝仍是深拷贝。个人意思是若是对象引用任什么时候候都不会被改变,那么不必使用深拷贝,只须要使用浅拷贝就好了。若是对象引用常常改变,那么就要使用深拷贝。没有一成不变的规则,一切都取决于具体需求。

原文出处


相关文章
相关标签/搜索