对Java中的深复制和浅复制的一些理解

来源:互联网 发布:一句话经典 知乎 编辑:程序博客网 时间:2024/06/06 07:40

Java语言的一个优点就是取消了指针的概念,但也导致了许多程序员在编程中常常忽略了对象与引用的区别,本文会试图澄清这一概念。并且由于Java不能通过简单的赋值来解决对象复制的问题,在开发过程中,也常常要要应用clone()方法来复制对象。

浅复制与深复制概念

浅复制(浅克隆) :被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。(即引用对象仍指向原对象,对引用对象进行修改,仍会导致原对象发生改变)

深复制(深克隆) :被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。

package com.zoer.src;  public class ObjRef {      Obj aObj = new Obj();      int aInt = 11;      public void changeObj(Obj inObj) {          inObj.str = "changed value";      }      public void changePri(int inInt) {          inInt = 22;      }      public static void main(String[] args) {          ObjRef oRef = new ObjRef();          System.out.println("Before call changeObj() method: " + oRef.aObj);          oRef.changeObj(oRef.aObj);          System.out.println("After call changeObj() method: " + oRef.aObj);          System.out.println("==================Print Primtive=================");          System.out.println("Before call changePri() method: " + oRef.aInt);          oRef.changePri(oRef.aInt);          System.out.println("After call changePri() method: " + oRef.aInt);      }  }  
package com.zoer.src;  public class Obj {      String str = "init value";      public String toString() {          return str;      }  }  

这段代码的主要部分调用了两个很相近的方法,changeObj()和changePri()。唯一不同的是它们一个把对象作为输入参数,另一个把Java中的基本类型int作为输入参数。并且在这两个函数体内部都对输入的参数进行了改动。看似一样的方法,程序输出的结果却不太一样。changeObj()方法真正的把输入的参数改变了,而changePri()方法对输入的参数没有任何的改变。
从这个例子知道Java对对象和基本的数据类型的处理是不一样的。和C语言一样,当把Java的基本数据类型(如int,char,double等)作为入口参数传给函数体的时候,传入的参数在函数体内部变成了局部变量,这个局部变量是输入参数的一个拷贝,所有的函数体内部的操作都是针对这个拷贝的操作,函数执行结束后,这个局部变量也就完成了它的使命,它影响不到作为输入参数的变量。这种方式的参数传递被称为”值传递”。而在Java中用对象作为入口参数的传递则缺省为”引用传递”,也就是说仅仅传递了对象的一个”引用”,这个”引用”的概念同c语言中的指针引用是一样的。当函数体内部对输入变量改变时,实质上就是在对这个对象的直接操作。
除了在函数传值的时候是”引用传递”,在任何用”=”向对象变量赋值的时候都是”引用传递”。就是类似于给变量再起一个别名。两个名字都指向内存中的同一个对象。
在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值,也就是说,A与B是两个独立的对象,但B的初始值是由A对象确定的。在Java语言中,用简单的赋值语句是不能满足这种需求的。要满足这种需求虽然有很多途径,但实现clone()方法是其中最简单,也是最高效的手段。
Java的所有类都默认继承java.lang.Object类,在java.lang.Object类中有一个方法clone()。JDK API的说明文档解释这个方法将返回Object对象的一个拷贝。要说明的有两点:一是拷贝对象返回的是一个新对象,而不是一个引用。二是拷贝对象与用new操作符返回的新对象的区别就是这个拷贝已经包含了一些原来对象的信息,而不是对象的初始信息。

一个典型的clone代码

public 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()方法。

深复制的一个例子

class UnCloneA implements Cloneable {      private int i;      public UnCloneA(int ii) {          i = ii;      }      public void doublevalue() {          i *= 2;      }      public String toString() {          return Integer.toString(i);      }      public Object clone() {          UnCloneA o = null;          try {              o = (UnCloneA) super.clone();          } catch (CloneNotSupportedException e) {              e.printStackTrace();          }          return o;      }  }  class CloneB implements Cloneable {      public int aInt;      public UnCloneA unCA = new UnCloneA(111);      public Object clone() {          CloneB o = null;          try {              o = (CloneB) super.clone();          } catch (CloneNotSupportedException e) {              e.printStackTrace();          }          o.unCA = (UnCloneA) unCA.clone();          return o;      }  }  public class CloneMain {      public static void main(String[] a) {          CloneB b1 = new CloneB();          b1.aInt = 11;          System.out.println("before clone,b1.aInt = " + b1.aInt);          System.out.println("before clone,b1.unCA = " + b1.unCA);          CloneB b2 = (CloneB) b1.clone();          b2.aInt = 22;          b2.unCA.doublevalue();          System.out.println("=================================");          System.out.println("after clone,b1.aInt = " + b1.aInt);          System.out.println("after clone,b1.unCA = " + b1.unCA);          System.out.println("=================================");          System.out.println("after clone,b2.aInt = " + b2.aInt);          System.out.println("after clone,b2.unCA = " + b2.unCA);      }  }  

输出结果:
before clone,b1.aInt = 11
before clone,b1.unCA = 111

=================================
after clone,b1.aInt = 11
after clone,b1.unCA = 111

=================================
after clone,b2.aInt = 22
after clone,b2.unCA = 222

可以看出,现在b2.unCA的改变对b1.unCA没有产生影响。此时b1.unCA与b2.unCA指向了两个不同的UnCloneA实例,而且在CloneB b2 = (CloneB)b1.clone();调用的那一刻b1和b2拥有相同的值,在这里,b1.i = b2.i = 11。

要知道不是所有的类都能实现深度clone的。例如,如果把上面的CloneB类中的UnCloneA类型变量改成StringBuffer类型,看一下JDK API中关于StringBuffer的说明,StringBuffer没有重载clone()方法,更为严重的是StringBuffer还是一个final类,这也是说我们也不能用继承的办法间接实现StringBuffer的clone。如果一个类中包含有StringBuffer类型对象或和StringBuffer相似类的对象,我们有两种选择:要么只能实现浅clone,要么就在类的clone()方法中加一句(假设是SringBuffer对象,而且变量名仍是unCA): o.unCA = new StringBuffer(unCA.toString()); //原来的是:o.unCA = (UnCloneA)unCA.clone();

Clone中String和StringBuffer的区别

下面的例子中包括两个类,CloneC类包含一个String类型变量和一个StringBuffer类型变量,并且实现了clone()方法。在StrClone类中声明了CloneC类型变量c1,然后调用c1的clone()方法生成c1的拷贝c2,在对c2中的String和StringBuffer类型变量用相应的方法改动之后打印结果:

class CloneC implements Cloneable {      public String str;      public StringBuffer strBuff;      public Object clone() {          CloneC o = null;          try {              o = (CloneC) super.clone();          } catch (CloneNotSupportedException e) {              e.printStackTrace();          }          return o;      }  }  public class StrClone {      public static void main(String[] a) {          CloneC c1 = new CloneC();          c1.str = new String("initializeStr");          c1.strBuff = new StringBuffer("initializeStrBuff");          System.out.println("before clone,c1.str = " + c1.str);          System.out.println("before clone,c1.strBuff = " + c1.strBuff);          CloneC c2 = (CloneC) c1.clone();          c2.str = c2.str.substring(0, 5);          c2.strBuff = c2.strBuff.append(" change strBuff clone");          System.out.println("=================================");          System.out.println("after clone,c1.str = " + c1.str);          System.out.println("after clone,c1.strBuff = " + c1.strBuff);          System.out.println("=================================");          System.out.println("after clone,c2.str = " + c2.str);          System.out.println("after clone,c2.strBuff = " + c2.strBuff);      }  }  

结果:
before clone,c1.strBuff = initializeStrBuff

=================================
after clone,c1.str = initializeStr
after clone,c1.strBuff = initializeStrBuff change strBuff clone

=================================
after clone,c2.str = initi
after clone,c2.strBuff = initializeStrBuff change strBuff clone

打印的结果可以看出,String类型的变量好象已经实现了深度clone,因为对c2.str的改动并没有影响到c1.str!难道Java把Sring类看成了基本数据类型?其实不然,这里有一个小小的把戏,秘密就在于c2.str = c2.str.substring(0,5)这一语句!实质上,在clone的时候c1.str与c2.str仍然是引用,而且都指向了同一个String对象。但在执行c2.str = c2.str.substring(0,5)的时候,它作用相当于生成了一个新的String类型,然后又赋回给c2.str。这是因为String被Sun公司的工程师写成了一个不可更改的类(immutable class),在所有String类中的函数都不能更改自身的值。

原型模式

定义:用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
类型:创建类模式
类图
这里写图片描述
原型模式主要用于对象的复制,它的核心是就是类图中的原型类Prototype。Prototype类需要具备以下两个条件:

  • 实现Cloneable接口。在java语言有一个Cloneable接口,它的作用只有一个,就是在运行时通知虚拟机可以安全地在实现了此接口的类上使用clone方法。在java虚拟机中,只有实现了这个接口的类才可以被拷贝,否则在运行时会抛出CloneNotSupportedException异常。
  • 重写Object类中的clone方法。Java中,所有类的父类都是Object类,Object类中有一个clone方法,作用是返回对象的一个拷贝,但是其作用域protected类型的,一般的类无法调用,因此,Prototype类需要将clone方法的作用域修改为public类型。

原型模式是一种比较简单的模式,也非常容易理解,实现一个接口,重写一个方法即完成了原型模式。在实际应用中,原型模式很少单独出现。经常与其他模式混用,他的原型类Prototype也常用抽象类来替代。

class Prototype implements Cloneable {        public Prototype clone(){            Prototype prototype = null;            try{                prototype = (Prototype)super.clone();            }catch(CloneNotSupportedException e){                e.printStackTrace();            }            return prototype;         }    }    class ConcretePrototype extends Prototype{        public void show(){            System.out.println("原型模式实现类");        }    }    public class Client {        public static void main(String[] args){            ConcretePrototype cp = new ConcretePrototype();            for(int i=0; i< 10; i++){                ConcretePrototype clonecp = (ConcretePrototype)cp.clone();                clonecp.show();            }        }    } 

使用原型模式创建对象比直接new一个对象在性能上要好的多,因为Object类的clone方法是一个本地方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。
使用原型模式的另一个好处是简化对象的创建,使得创建对象就像我们在编辑文档时的复制粘贴一样简单。
因为以上优点,所以在需要重复地创建相似对象时可以考虑使用原型模式。比如需要在一个循环体内创建对象,假如对象创建过程比较复杂或者循环次数很多的话,使用原型模式不但可以简化创建过程,而且可以使系统的整体性能提高很多。

注意事项

  • 使用原型模式复制对象不会调用类的构造方法。因为对象的复制是通过调用Object类的clone方法来完成的,它直接在内存中复制数据,因此不会调用到类的构造方法。不但构造方法中的代码不会执行,甚至连访问权限都对原型模式无效。单例模式中,只要将构造方法的访问权限设置为private型,就可以实现单例。但是clone方法直接无视构造方法的权限,所以,单例模式与原型模式是冲突的,在使用时要特别注意。
  • 深拷贝与浅拷贝。Object类的clone方法只会拷贝对象中的基本的数据类型(8种基本数据类型byte,char,short,int,long,float,double,boolean),对于数组、容器对象、引用对象等都不会拷贝,这就是浅拷贝。如果要实现深拷贝,必须将原型模式中的数组、容器对象、引用对象等另行拷贝。例如:
public class Prototype implements Cloneable {        private ArrayList list = new ArrayList();        public Prototype clone(){            Prototype prototype = null;            try{                prototype = (Prototype)super.clone();                prototype.list = (ArrayList) this.list.clone();            }catch(CloneNotSupportedException e){                e.printStackTrace();            }            return prototype;         }    }    

由于ArrayList不是基本类型,所以成员变量list,不会被拷贝,需要我们自己实现深拷贝,幸运的是Java提供的大部分的容器类都实现了Cloneable接口。所以实现深拷贝并不是特别困难。