`
ranyut
  • 浏览: 259782 次
  • 性别: Icon_minigender_1
  • 来自: 重庆
社区版块
存档分类
最新评论

Java克隆clone浅拷贝与深拷贝

    博客分类:
  • Java
阅读更多

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

    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++的指针的感觉吗?
分享到:
评论
4 楼 liyanboss 2014-07-04  
因为Object的clone方法使用了RTTI(运行时类型识别)机制,可以动态的找到目前正在调用clone方法的那个引用,并找到它指向的对象,然后根据这个对象的大小去申请内存空间,然后进行bitwise(逐位)的复制, 将该对象的内存空间完全复制到新的空间中去,
从而达到克隆的目的.
即,谁调用clone方法,就克隆这引用指向的对象
子类没有clone方法,使用this.clone()则直接异常,,因为是子类继承Object的
比如你有一个Student类实现了Cloneable接口,重写了clone方法,但是重写的clone方法里的执行super.clone,并不是说和自身无关,
例如: Student stu=new student();
Student stu1=(Student)stu.clone();
当执行stu.clone();时候,虽然clone方法里面执行的是super.clone(),但是Object的clone方法会寻找当前正在调用clone方法的那个引用,这里这个引用就是stu,然后自动的去找stu指向的对象,然后进行逐位赋值,从而达到克隆的目的,
3 楼 liyanboss 2014-07-04  
因为clone是一个受保护的方法,同一个包中可见。按照惯例,返回的对象应该通过调用 super.clone 获得。   这样回答,对吗?
2 楼 liyanboss 2014-07-04  
最后一个问题的答案呢》?
1 楼 uule 2014-04-10  
骚年,最后那个 问题1:是如何解决的?

相关推荐

    浅拷贝(浅复制、浅克隆)、深拷贝(深复制、深克隆)实战工程

    在编程领域,尤其是在Java语言中,对象的复制是常见的操作,这涉及到两个主要概念:浅拷贝(浅复制、浅克隆)和深拷贝(深复制、深克隆)。这两个概念是理解对象生命周期和内存管理的关键部分。下面将详细阐述它们的...

    java深入理解浅拷贝和深拷贝

    在Java编程中,深入理解浅拷贝和深拷贝是非常重要的概念,特别是在处理对象复制时。浅拷贝和深拷贝的区别在于复制对象时对内存中数据的处理方式。 浅拷贝(Shallow Copy)指的是创建一个新的对象,该对象与原对象...

    java设计模式【之】原型模式、深拷贝与浅拷贝【源码】【场景:克隆羊】

    java设计模式【之】原型模式、深拷贝与浅拷贝【源码】【场景:克隆羊】 * 原型模式(Prototype) * 实现方式: * 需要被克隆的 class类, 重写Object中的clone()方法,并实现Cloneable接口(否则报错 ...

    java对象的深拷贝和浅拷贝[归类].pdf

    在Java编程中,对象拷贝是常见的操作,主要分为两种类型:浅拷贝(Shallow Copy)和深拷贝(Deep Copy)。这两种拷贝方式在处理对象时有着本质的区别,对于理解对象复制机制以及在实际开发中正确地复制复杂对象至关...

    java中的指针,引用及对象的clone

    本文将深入探讨Java中的引用以及对象的克隆机制,包括浅拷贝和深拷贝的区别及其应用场景。 #### 二、Java中的引用 在Java中,当使用`new`关键字创建一个对象时,实际上创建的是一个指向该对象的引用。例如,以下...

    java clone

    默认的`clone`方法是浅拷贝,如果需要深拷贝,需要在子类中自行处理。 下面是一些关于`clone`的实践心得: 1. 注意权限:默认情况下,`clone`方法是`protected`的,这意味着在同一个包内的类才能直接调用。如果需要...

    java List 深度复制方法

    在Java中,当我们使用`clone()`方法或`System.arraycopy()`进行复制时,通常得到的是浅复制的结果。这意味着原始对象和复制对象中的引用指向内存中的同一块区域。如果List中的元素是基本类型,这种复制方式是没问题...

    java clone的小例子

    当我们修改克隆对象`student2`的年龄时,原始对象`student1`的属性保持不变,这显示了`clone()`创建的是对象的浅拷贝。 需要注意的是,对于包含引用类型的字段,`clone()`只会复制引用,而不是引用的对象。这意味着...

    Java中clone方法共6页.pdf.zip

    然而,需要注意的是,简单地调用`clone()`并不总是能创建出完全独立的对象,尤其是当对象内部包含引用其他对象时,这种情况下只进行了浅拷贝,而不是深拷贝。 1. **浅拷贝与深拷贝**: - 浅拷贝:当一个对象被复制...

    android 浅复制和深复制-Java Generic Deep Copy 篇

    本文将深入探讨Java中的浅复制和深复制,并以Android为背景,结合具体的实例讲解如何实现Java泛型深拷贝。 首先,我们要理解浅复制和深复制的概念。浅复制(Shallow Copy)是指创建一个新的对象,然后将原对象引用...

    Java对象的复制克隆

    在Java中,实现深拷贝通常需要自定义拷贝构造函数或重写 `clone()` 方法。例如: ```java class Container implements Cloneable { String str = "Hello"; @Override protected Object clone() throws ...

    java对象复制克隆

    标题中的“java对象复制克隆”主要涉及的是深拷贝,这是一种创建一个新对象的方式,新对象不仅复制了原对象的所有属性,还复制了嵌套的对象引用,使得修改复制后的对象不会影响到原始对象。 **浅拷贝**:在浅拷贝中...

    java Clone

    Java中的`clone`方法是Java语言提供的一种复制对象的方式,它允许创建一个对象的副本,这个副本与原对象具有相同的属性值,但它们是两个独立的对象,修改副本不会影响原对象。`clone`方法存在于Java的`java.lang....

    关于java clone的一些基本的概念

    在Java编程语言中,`clone`是一个非常重要的概念,它涉及到对象复制和对象的深拷贝与浅拷贝。本文将深入探讨Java中的`clone`方法及其相关知识点。 首先,`clone`方法是Java `Object`类的一个成员方法,定义为`...

    java的深度克隆的例子

    首先,Java提供了两种基本的克隆方式:浅克隆(shallow clone)和深克隆。浅克隆仅仅复制对象本身,而不复制对象所引用的对象。而深克隆则会递归地复制对象及所有引用的对象,确保新创建的对象和原始对象及其引用的...

    结合JVM解读浅拷贝和深拷贝

    总的来说,了解`Cloneable`接口、`Object.clone()`方法以及浅拷贝和深拷贝的概念对于编写高质量、健壮的Java代码是至关重要的。正确使用这些机制能够帮助开发者创建可维护、可扩展的系统,并确保数据的正确复制和...

    clone()方法示例(对象克隆)_对象克隆_nervouse78_源码

    总的来说,`clone()`方法是Java中一种快速创建对象副本的手段,但它需要开发者对浅拷贝和深拷贝有清晰的理解,并根据实际需求适当地处理对象的属性。通过`nervouse78`的示例,你可以学习到如何在实际项目中运用`...

    Java深复制与浅复制&Clone

    但是,`clone()`方法默认的是浅复制,且需要被克隆的对象实现`Cloneable`接口。以下是一个简单的例子: ```java public class MyClass implements Cloneable { private String str; // ... getters and setters ...

Global site tag (gtag.js) - Google Analytics