天天看点

Java克隆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接口,因此拷贝操作总会抛出异常。然而在大部分情况下为你的类规划和实现一个clone方法因而可以恰当的拷贝会更好。

另外一点就是你可以支持无条件的和有条件的拷贝。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成员的用法:

    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是相同的)

版权声明
  评论人:lmgl    参与分: 3    专家分: 0 发表时间: 2002-12-6 上午10:58

为什么以下的代码结果是"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]);

  评论人:cherami    参与分: 19726    专家分: 4830 发表时间: 2002-12-6 下午1:27

那是深度拷贝和浅拷贝的问题,请注意文章中的以下内容:

如果两个对象共享一个引用,就像CloneDemo4中的情况一样,那么通常你会遇到问题,除非那个引用是只读的,要避开这个问题,你需要实现clone方法处理这个问题。这种情况的另一种说法是

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

  评论人:kert    参与分: 558    专家分: 520 发表时间: 2002-12-6 下午1:31

Java的Clone应该是克隆对象的值,而不是引用!

然而如果不清楚哪是对象的值,则对clone的结果便会感到疑惑!

看如下的几个例子!

1.

private static void test1() {

        pl("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]);

        pl("");

}

我们在这里clone了一个数组对象,数组的元素是String,因此这里String是这个s的值。

当我们clone s时,Java生成了两个同样的String。并实现了我们想要作的。

输出为:

[pre]

s: [Ljava.lang.String;@7172ea

ss: [Ljava.lang.String;@2f6684

ss[0]=b

s[0]=a

[/pre]

2.就是同上个回复一样的例子

private static void test2() {

        pl("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]);

        pl("");

}

这里我们想要clone一个二维数组,并且[i]希望[/i]同样clone数组的每个值。

然而结果却事与愿违;

[pre]

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

[/pre]

然而事实上结果却很好的符合的clone的原则。Java很好的clone了我们的二维数组s。并生成了它的一个副本ss。

而且ss中的每个值(一维数组)与原来的值完全相同。(我们可以从输出中清楚地看到)。

因为对于一个数组对象而言,数组的元素就是这个对象的值。因此二维数组的元素(值)就是一维数组。

而对于数组对象,Java把它作为类似指针处理。

3.

private static void test3(){

        pl("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]);

        pl("");

}

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

[pre]

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

[/pre]

---------

对于普通的clone而言(浅度clone),虚拟机使用本地方法,用类似copy内存的方式复制一个对象。因此会产生似乎是令人迷惑的结果。

因此通常clone数组或是复杂对象时,需要使用深度clone,一一clone每一个对象的元素。

  评论人:xjw-aaa    参与分: 788    专家分: 330 发表时间: 2003-11-18 下午3:05

cherami sir:

  问题1:

    为什么数组直接用.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());      

        }    

    }

问题2:

     我想问一下,克隆一个对象是不是给克隆对象从新分配一个内存空间 ;而引用一个对象是指同一内存不同有不同对象名。对象名有点像c++的指针的感觉吗?