`
uule
  • 浏览: 6306470 次
  • 性别: Icon_minigender_1
  • 来自: 一片神奇的土地
社区版块
存档分类
最新评论

Prototype模式中的深拷贝浅拷贝

 
阅读更多

深拷贝与浅拷贝

 

浅拷贝(Shallow Copy):

①对于数据类型是【基本数据类型】的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。

②对于数据类型是【引用数据类型】的成员变量,比如说成员变量是某个【数组】、某个【类的对象】等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为

【实际上两个对象的成员变量都指向同一个实例】。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。

 

Object类中有一个方法为protected Object clone() throws CloneNotSupportedException,这个方法就是进行的浅拷贝。

有了这个浅拷贝模板,我们可以通过【调用clone()方法来实现对象的浅拷贝】。但是需要注意:

1、Object类虽然有这个方法,但是【这个方法是受保护的(被protected修饰),所以我们无法直接使用】。

2、使用clone方法的类【必须实现Cloneable接口】,否则会抛出异常CloneNotSupportedException。对于这两点,我们的解决方法是,在要使用clone方法的类中重写clone()方法,通过super.clone()调用Object类中的原clone方法。

 

必须【显式的指定clone方法】并且你的类【必须实现Cloneable接口】。

Cloneable是“标记”接口的一个范例,接口自身不指定任何东西,但是,Object.clone检查类是否实现了它,如果没有就抛出一个CloneNotSupportedException异常。

 

深拷贝:设想一下,一个类有一个对象,其成员变量中又有一个对象,该对象指向另一个对象,另一个对象又指向另一个对象,直到一个确定的实例。这就形成了对象图。那么,对于深拷贝来说,不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷贝!

简单地说,深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间。

一个对象包含一个HashMap引用,当对象被拷贝时,HashMap引用也被拷贝了,这意味着拷贝生成的那个对象包含那个HashMap对象的原始引用。因此当原始对象中的HashMap的内容发生变化,拷贝生成的对象中的那个HashMap的内容也同时更新。

 

Object.clone完成的是对象的“浅”拷贝,即简单的成员到成员的拷贝。它不做“深度”拷贝,即成员或者数组指向的对象的递归拷贝。

 

===========================================================

假设在你的应用中使用一些对象,你如何拷贝你的对象呢?最明显的方法是讲一个对象简单的赋值给另一个,就像这样:

    obj2 = obj1;

但是这个方法实际上没有拷贝对象而仅仅是拷贝了一个对象引用,换换言之,在你执行这个操作后仍然只有一个对象,但是多出了一个对该对象的引用。
如果这个看似明显的方法不能正常工作,那么如何实际的拷贝一个对象呢?为什么不试试Object.clone呢?这个方法对Object的所有子类都是可用的。例如:

    class A {
        private int x;
        public A(int i) {
            x = i;
        }
    }
    
    public class CloneDemo1 {
        public static void main(String args[]) throws CloneNotSupportedException {
            A obj1 = new A(37);
            A obj2 = (A)obj1.clone();
        }
    }

 这个代码引发一个编译错误,因为Object.clone是一个protected方法。那么再试一次,换一种方法:

class A {
        private int x;
        public A(int i) {
            x = i;
        }
        public Object clone() {
            try {
                return super.clone();
            }
            catch (CloneNotSupportedException e) {
                throw new InternalError(e.toString());
            }
        }
    }
    
    public class CloneDemo2 {
        public static void main(String args[])
          throws CloneNotSupportedException {
            A obj1 = new A(37);
            A obj2 = (A)obj1.clone();
        }
    }

 在这个方法中,呢定义自己的clone方法,它扩展Object.clone方法,CloneDemo2可以编译,但是当你运行它时会抛出一个CloneNotSupportedException异常。

这里仍然缺少一些东西,你必须让那些包含clone方法的类实现Cloneable接口,就像这样:

class A implements Cloneable {
        private int x;
        public A(int i) {
            x = i;
        }
        public Object clone() {
            try {
                return super.clone();
            }
            catch (CloneNotSupportedException e) {
                throw new InternalError(e.toString());
            }
        }
        public int getx() {
            return x;
        }
    }
    
    public class CloneDemo3 {
        public static void main(String args[])
          throws CloneNotSupportedException {
            A obj1 = new A(37);
            A obj2 = (A)obj1.clone();
            System.out.println(obj2.getx());
        }
    }

 成功了!CloneDemo3可以编译并产生期望的结果:
37   

你已经了解了必须显式的指定clone方法并且你的类必须实现Cloneable接口。Cloneable是“标记”接口的一个范例,接口自身不指定任何东西,但是,Object.clone检查类是否实现了它,如果没有就抛出一个CloneNotSupportedException异常。

Object.clone方法做简单的拷贝操作,将一个对象的所有成员变量拷贝到一个新的对象。在CloneDemo3中,A.clone调用Object.clone,然后Object.clone创建一个新的A对象并将已经存在的那个对象的成员变量的内容拷贝到那个新对象。
CloneDemo3中还有大量其他值得考虑的东西。其中之一就是你可以防止你写的类的用户拷贝那个类对象。为了做到这个,你可以不实现Cloneable接口,因此拷贝操作总会抛出异常。

另外一点就是你可以支持无条件的和有条件的拷贝。CloneDemo3是无条件的支持拷贝,clone方法不会传播CloneNotSupportedException异常。

一个更通用的方法是有条件的支持类拷贝。在这种情况下,对象自身可以被拷贝,但是对象的子类可能不能拷贝。对于有条件拷贝,clone方法必须申明它能够传播CloneNotSupportedException异常。有条件拷贝的一个范例是一个集合类的对象的元素只有在那些元素是可以拷贝的时候才能进行拷贝。

有条件拷贝的另外的一种方式是实现一个合适的clone方法但是不实现Cloneable接口。在这种情况下,如果愿意,子类可以支持拷贝操作。

拷贝操作可能是很棘手的,因为Object.clone做简单的对象成员拷贝,有时候这不是你期望的,例如:

import java.util.*;
    
    class A implements Cloneable {
        public HashMap map;
        public A() {
            map = new HashMap();
            map.put("key1", "value1");
            map.put("key2", "value2");
        }
        public Object clone() {
            try {
                return super.clone();
            }
            catch (CloneNotSupportedException e) {
                throw new InternalError(e.toString());
            }
        }
    }
    
    public class CloneDemo4 {
        public static void main(String args[]) {
            A obj1 = new A();
            A obj2 = (A)obj1.clone();
    
            obj1.map.remove("key1");
    
            System.out.println(obj2.map.get("key1"));
        }
    }

 你可能希望CloneDemo4显示如下的结果:
value1
但是实际上它显示:
null

 

发生了什么事?在CloneDemo4中,一个对象包含一个HashMap引用,当对象被拷贝时,HashMap引用也被拷贝了,这意味着拷贝生成的那个对象包含那个HashMap对象的原始引用。因此当原始对象中的HashMap的内容发生变化,拷贝生成的对象中的那个HashMap的内容也同时更新。

 

要修正这个问题,你可以让clone方法更完善:

import java.util.*;
    
    class A implements Cloneable {
        public HashMap map;
        public A() {
            map = new HashMap();
            map.put("key1", "value1");
            map.put("key2", "value2");
        }
        public Object clone() {
            try {
                A aobj = (A)super.clone();
                aobj.map = (HashMap)map.clone();
                return aobj;
            }
            catch (CloneNotSupportedException e) {
                throw new InternalError(e.toString());
            }
        }
    }
    
    public class CloneDemo5 {
        public static void main(String args[]) {
            A obj1 = new A();
            A obj2 = (A)obj1.clone();
    
            obj1.map.remove("key1");
    
            System.out.println(obj2.map.get("key1"));
        }
    }

 Clone5Demo显示如下的期望的结果:
value1   

Clone5Demo调用super.clone创建一个A对象并拷贝map成员,然后调用HashMap.clone完成HashMap类型的拷贝。这个操作包含创建一个新的hash表并且从老的那个里面拷贝成员到那个新的hash表。

如果两个对象共享一个引用,就像CloneDemo4中的情况一样,那么通常你会遇到问题,除非那个引用是只读的,要避开这个问题,你需要实现clone方法处理这个问题。这种情况的另一种说法是
Object.clone完成的是对象的“浅”拷贝,即简单的成员到成员的拷贝。它不做“深度”拷贝,即成员或者数组指向的对象的递归拷贝。

不使用"new CloneDemo5"创建一个对象,那么调用super.clone就是极度重要的。你应该在类层次的每一级上调用super.clone。这是因为每一级都可能有它自己的共享对象问题。如果你使用"new"而不是super.clone,那么你的代码对于那些从你的类继承的子类是不正确的,那些代码调用你的clone方法但是收到一个不正确的返回类型。

关于拷贝,另一个需要知道的事情是可以拷贝任何数组,只需简单的调用clone方法:

public class CloneDemo6 {
        public static void main(String args[]) {
            int vec1[] = new int[]{1, 2, 3};
            int vec2[] = (int[])vec1.clone();
            System.out.println(vec2[0] + " " + vec2[1] +
                " " + vec2[2]);
        }
    }

 Final:

关于拷贝的最后一个重要的事情是:它是创建和初始化一个新对象的方式,但是它不同于调用一个构造方法。这个区别的一个例子是它事关空的final成员,也就是那些声明为"final"但是没有初始化的成员,它们只能在构造方法中被赋值。下面是一个空的final成员的用法:

public class CloneDemo7 {
    
        private int a;
        private int b;
        private final long c;
    
        public CloneDemo7(int a, int b) {
            this.a = a;
            this.b = b;
            this.c = System.currentTimeMillis();
        }

        public static void main(String args[]) {
            CloneDemo7 obj = new CloneDemo7(37, 47);
        }
    }

 在CloneDemo7的构造方法中,一个final成员"c"从系统时钟中获得一个时戳。如果你拷贝这样的类型的值你想得到什么?Object.clone拷贝所有的成员变量,但是你想那个时戳成员被设置为当前系统时钟的值。然而,如果一个成员是final类型的,你只能在构造方法中设置那个成员,不能在clone方法中。下面是这个问题的例子:

public class CloneDemo8 {
    
        private int a;
        private int b;
        private final long c;
    
        public CloneDemo8(int a, int b) {
            this.a = a;
            this.b = b;
            this.c = System.currentTimeMillis();
        }
    
        public CloneDemo8(CloneDemo8 obj) {
            this.a = obj.a;
            this.b = obj.b;
            this.c = System.currentTimeMillis();
        }
    
        public Object clone() throws CloneNotSupportedException {
            //this.c = System.currentTimeMillis();
            return super.clone();
        }

        public static void main(String args[]) {
            CloneDemo8 obj = new CloneDemo8(37, 47);
            CloneDemo8 obj2 = new CloneDemo8(obj);
        }
    }

 如果你想取消final成员的赋值语句那一行的注释程序就不能编译。对于这样的问题,我们不使用clone方法,范例程序使用拷贝构造方法。拷贝构造方法的参数是和它自身类型相同并实现合适的拷贝逻辑。(译者注:在实现拷贝构造方法时需要注意共享对象问题,由于范例中的其他两个成员都是原始类型所以没有问题,但是如果你自己的类的成员的类型是对象类型就不能使用直接赋值也要使用拷贝进行或者是其他合适的拷贝构造方法,但是如果你需要使用的类型没有拷贝方法或者合适的拷贝构造方法,那么你就不能写你自己的合适的拷贝构造方法或者拷贝方法,所辛的是java的核心类基本上不存在这个问题,但是你如果使用其他的人的类就不好说了,因此如果你写自己的类并想让很多人用,那么你一定要实现合适的拷贝方法)

也许你认为你可以不使用空final成员而是在声明那些final成员的时候马上使用系统时间来初始化解决这样的问题,就像下面这样:

class A implements Cloneable {
        final long x = System.currentTimeMillis();
        public Object clone() {
            try {
                return super.clone();
            }
            catch (CloneNotSupportedException e) {
                throw new InternalError(e.toString());
            }
        }
    }
    
    public class CloneDemo9 {
        public static void main(String args[]) {
            A obj1 = new A();
            // sleep 100 ms before doing clone,
            // to ensure unique timestamp
            try {
                Thread.sleep(100);
            }
            catch (InterruptedException e) {
                System.err.println(e);
            }
            A obj2 = (A)obj1.clone();
            System.out.println(obj1.x + " " + obj2.x);
        }
    }

 这样同样不能工作,当你运行这个程序,你可以看到obj1.x和obj2.x有相同的值。这指出当一个对象是拷贝生成的时候通常的对象初始化没有进行并且你不能在clone方法中设置final成员的值。因此如果简单的拷贝操作不能正确的初始化一个成员,你就不应该将它声明为final的。或者你需要使用拷贝构造方法作为拷贝的替代方法。(译者注:如果你将成员声明为private并且不提供修改它的值方法,那么效果和将它声明为final是相同的)

 

一些问题:

1、为什么以下的代码结果是"aaaa"

String[][] ss=new String[][]{{"342","fs"},{"32","ger"}};
        String[][] s;
        s = (String[][])ss.clone();
        ss[1][1]="aaaa";
        System.out.println(s[1][1]);

 如果两个对象共享一个引用,就像CloneDemo4中的情况一样,那么通常你会遇到问题,除非那个引用是只读的,要避开这个问题,你需要实现clone方法处理这个问题。这种情况的另一种说法是
Object.clone完成的是对象的“浅”拷贝,即简单的成员到成员的拷贝。它不做“深度”拷贝,即成员或者数组指向的对象的递归拷贝。

 

2、

private static void test1() {

        String[] s = {"a", "a"};
        String[] ss = (String[]) s.clone();
        pl("s: " + s);
        pl("ss: " + ss);
        ss[0] = "b";
        pl("ss[0]=" + ss[0]);
        pl("s[0]=" + s[0]);

}

 我们在这里clone了一个数组对象,数组的元素是String,因此这里String是这个s的值。
当我们clone s时,Java生成了两个同样的String。并实现了我们想要作的。
输出为:
s: [Ljava.lang.String;@7172ea
ss: [Ljava.lang.String;@2f6684
ss[0]=b
s[0]=a

 

 3、

private static void test2() {
        String[][] s = {
            {"a", "a"},
            {"a", "a"}
        };
        String[][] ss = (String[][]) s.clone();
        pl("s="+s);
        pl("ss="+ss);
		
        pl("s[0]="+s[0]);
        pl("ss[0]="+ss[0]);
        pl("s[1]="+s[1]);
        pl("ss[1]="+ss[1]);
		
        ss[0][0]="b";
        pl("s[0][0]="+s[0][0]);
        pl("ss[0][0]="+ss[0][0]);
}

 这里我们想要clone一个二维数组,并且[i]希望[/i]同样clone数组的每个值。
然而结果却事与愿违;
test2-----------
s=[[Ljava.lang.String;@738798
ss=[[Ljava.lang.String;@4b222f
s[0]=[Ljava.lang.String;@3169f8
ss[0]=[Ljava.lang.String;@3169f8
s[1]=[Ljava.lang.String;@2457b6
ss[1]=[Ljava.lang.String;@2457b6
s[0][0]=b
ss[0][0]=b

然而事实上结果却很好的符合的clone的原则。Java很好的clone了我们的二维数组s。并生成了它的一个副本ss。
而且ss中的每个值(一维数组)与原来的值完全相同。(我们可以从输出中清楚地看到)。
因为对于一个数组对象而言,数组的元素就是这个对象的值。因此二维数组的元素(值)就是一维数组。
而对于数组对象,Java把它作为类似指针处理。

 

 4、

private static void test3(){

        String[][] s = {
            {"a", "a"},
            {"a", "a"}
        };
        String[][]ss={
          (String[]) s[0].clone(),
          (String[]) s[1].clone()
        };
        pl("s="+s);
        pl("ss="+ss);
		
        pl("s[0]="+s[0]);
        pl("ss[0]="+ss[0]);
        pl("s[1]="+s[1]);
        pl("ss[1]="+ss[1]);
		
        ss[0][0]="b";
        pl("s[0][0]="+s[0][0]);
        pl("ss[0][0]="+ss[0][0]);
}

 而当我们换一种clone方式(深度clone),我们可以看到结果发生了变化

test3-----------
s=[[Ljava.lang.String;@7a78d3
ss=[[Ljava.lang.String;@129206
s[0]=[Ljava.lang.String;@30f13d
ss[0]=[Ljava.lang.String;@2e000d
s[1]=[Ljava.lang.String;@55af5
ss[1]=[Ljava.lang.String;@169e11
s[0][0]=a
ss[0][0]=b

 

对于普通的clone而言(浅度clone),虚拟机使用本地方法,用类似copy内存的方式复制一个对象。因此会产生似乎是令人迷惑的结果。
因此通常clone数组或是复杂对象时,需要使用深度clone,一一clone每一个对象的元素。

 

 一个疑惑:

为什么数组直接用.clone()方法。而自定义的A类却用 super.clone()方法。我觉得
用super.clone()方法,不符合面向对象思想。我想克隆的是A类的副本,而super.clone()方法克隆的是父类的对象,然后再用A obj2 = (A)obj1.clone()强制转换为A类。自定义的A类默认不就是继承Object类吗?它本身不就可以直接使用clone()方法吗,为什么还用super.clone()方法。

  于是我将super.clone()换成this.clone(),编译通过。但运行抛出at A.clone(CloneDemo3.java:12)类外,您能不能给我一个合理的解释。

 

class A implements Cloneable 
{      
    private int x; 
    
    public A(int i) 
    { 
            x = i; 
    }  
    public Object clone() 
    {           
        try {                
            return this.clone();
		   //我将super换成this                      

		   //return super.clone();
        }      
        catch (Exception e)
        {                
            throw new InternalError(e.toString()); 
        }      
    } 
    public int getx() 
    {
        return x;
    }  
}       
    
    public class CloneDemo3
    {      
        public static void main(String args[])throws Exception 
       {           
        A obj1 = new A(37); 
        A obj2 = (A)obj1.clone();            
        System.out.println(obj2.getx());      
        }    
    }

 ?

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics