• java 浅拷贝和深拷贝 对象克隆clone


    分一下几点讨论:

    • 为什么要克隆?
    • 如何实现克隆
    • 浅克隆和深克隆
    • 解决多层克隆问题
    • 总结

    一:为什么要克隆?

      大家先思考一个问题,为什么需要克隆对象?直接new一个对象不行吗?

      答案是:克隆的对象可能包含一些已经修改过的属性,而new出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠clone方法了。那么我把这个对象的临时属性一个一个的赋值给我新new的对象不也行嘛?可以是可以,但是一来麻烦不说,二来,大家通过上面的源码都发现了clone是一个native方法,就是快啊,在底层实现的。

      提个醒,我们常见的Object a=new Object();Object b;b=a;这种形式的代码复制的是引用,即对象在内存中的地址,a和b对象仍然指向了同一个对象。

      而通过clone方法赋值的对象跟原来的对象时同时独立存在的。

    二:如何实现克隆 

      Object提供了一个对象拷贝的默认方法clone方法,但是该方法是有缺陷的,它提供了一种浅拷贝方式,也就是它并不会把对象所有属性全部拷贝一份,而是有选择性的拷贝,拷贝规则如下:

    1、基本类型

      如果变量是基本类型,则拷贝其值,比如:int、float、long等。

    2、String字符串

      这个比较特殊,拷贝的是地址,是个引用,但是在修改的时候,它会从字符串池(String Pool)中重新生成新的字符串,原有的字符串对象保持不变,此处可以认为String是个基本类型。

    3、对象

      如果变量时一个实例对象,则拷贝地址引用,也就是说此时新拷贝出的对象与原有对象共享该实例变量,不受访问权限的限制。这在Java中很疯狂,因为它突破了访问权限的定义,一个private修饰的变量,竟然可以被两个实例对象访问。

      Cloneable接口如下:如果调用Object的clone方法,没有实现Cloneable接口,则会抛出CloneNotSupportedException

    三:浅克隆和深克隆

    1、浅克隆

      在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。

      简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。

      

      上图中person是一个对象,浅拷贝会导致拷贝的对象和之前的对象持有相同的person对象的地址引用,所以修改操作会互相影响。

      在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。 

    2、深克隆

      在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。

      简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。

      上图中的person对象,深拷贝可以达到拷贝之后的person对象是独立的,互不影响。

      在Java语言中,如果需要实现深克隆,可以通过覆盖Object类的clone()方法实现,也可以通过序列化(Serialization)等方式来实现。

      (如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。

      序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。

    扩展
      Java语言提供的Cloneable接口和Serializable接口的代码非常简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。

     分别举例两个浅拷贝和深拷贝的例子:

    class Student implements Cloneable{  
        private int number;  
      
        public int getNumber() {  
            return number;  
        }  
      
        public void setNumber(int number) {  
            this.number = number;  
        }  
          
        @Override  
        public Object clone() {  
            Student stu = null;  
            try{  
                stu = (Student)super.clone();  
            }catch(CloneNotSupportedException e) {  
                e.printStackTrace();  
            }  
            return stu;  
        }  
    }  
    public class Test {  
        public static void main(String args[]) {  
            Student stu1 = new Student();  
            stu1.setNumber(12345);  
            Student stu2 = (Student)stu1.clone();  
              
            System.out.println("学生1:" + stu1.getNumber());  
            System.out.println("学生2:" + stu2.getNumber());  
              
            stu2.setNumber(54321);  
          
            System.out.println("学生1:" + stu1.getNumber());  
            System.out.println("学生2:" + stu2.getNumber());  
        }  
    }

    结果:

      学生1:12345  

      学生2:12345  

      学生1:12345  

      学生2:54321

      如果你还不相信这两个对象不是同一个对象,那么你可以看看这一句:

    System.out.println(stu1 == stu2); // false  

      上面的复制被称为浅克隆。

      还有一种稍微复杂的深度复制:

      我们在学生类里再加一个Address类。

    class Address  {  
        private String add;  
      
        public String getAdd() {  
            return add;  
        }  
      
        public void setAdd(String add) {  
            this.add = add;  
        }  
          
    }  
      
    class Student implements Cloneable{  
        private int number;  
      
        private Address addr;  
          
        public Address getAddr() {  
            return addr;  
        }  
      
        public void setAddr(Address addr) {  
            this.addr = addr;  
        }  
      
        public int getNumber() {  
            return number;  
        }  
      
        public void setNumber(int number) {  
            this.number = number;  
        }  
          
        @Override  
        public Object clone() {  
            Student stu = null;  
            try{  
                stu = (Student)super.clone();  
            }catch(CloneNotSupportedException e) {  
                e.printStackTrace();  
            }  
            return stu;  
        }  
    }  
    public class Test {  
          
        public static void main(String args[]) {  
              
            Address addr = new Address();  
            addr.setAdd("杭州市");  
            Student stu1 = new Student();  
            stu1.setNumber(123);  
            stu1.setAddr(addr);  
              
            Student stu2 = (Student)stu1.clone();  
              
            System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
            System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  
        }  
    }

    结果:

      学生1:123,地址:杭州市  

      学生2:123,地址:杭州市   

    乍一看没什么问题,真的是这样吗?

    我们在main方法中试着改变addr实例的地址。

    addr.setAdd("西湖区");  
      
    System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
    System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  

    结果:

    学生1:123,地址:杭州市  
    学生2:123,地址:杭州市  
    学生1:123,地址:西湖区  
    学生2:123,地址:西湖区  

      这就奇怪了,怎么两个学生的地址都改变了?

      原因是浅复制只是复制了addr变量的引用,并没有真正的开辟另一块空间,将值复制后再将引用返回给新对象。

      所以,为了达到真正的复制对象,而不是纯粹引用复制。我们需要将Address类可复制化,并且修改clone方法,完整代码如下:

    class Address implements Cloneable {  
        private String add;  
      
        public String getAdd() {  
            return add;  
        }  
      
        public void setAdd(String add) {  
            this.add = add;  
        }  
          
        @Override  
        public Object clone() {  
            Address addr = null;  
            try{  
                addr = (Address)super.clone();  
            }catch(CloneNotSupportedException e) {  
                e.printStackTrace();  
            }  
            return addr;  
        }  
    }  
      
    class Student implements Cloneable{  
        private int number;  
      
        private Address addr;  
          
        public Address getAddr() {  
            return addr;  
        }  
      
        public void setAddr(Address addr) {  
            this.addr = addr;  
        }  
      
        public int getNumber() {  
            return number;  
        }  
      
        public void setNumber(int number) {  
            this.number = number;  
        }  
          
        @Override  
        public Object clone() {  
            Student stu = null;  
            try{  
                stu = (Student)super.clone();   //浅复制  
            }catch(CloneNotSupportedException e) {  
                e.printStackTrace();  
            }  
            stu.addr = (Address)addr.clone();   //深度复制  
            return stu;  
        }  
    }  
    public class Test {  
          
        public static void main(String args[]) {  
              
            Address addr = new Address();  
            addr.setAdd("杭州市");  
            Student stu1 = new Student();  
            stu1.setNumber(123);  
            stu1.setAddr(addr);  
              
            Student stu2 = (Student)stu1.clone();  
              
            System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
            System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  
              
            addr.setAdd("西湖区");  
              
            System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
            System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  
        }  
    }

    结果:

    学生1:123,地址:杭州市  
    学生2:123,地址:杭州市  
    学生1:123,地址:西湖区  
    学生2:123,地址:杭州市  

    这样结果就符合我们的想法了。

    四:解决多层克隆问题

      如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆

    public class Outer implements Serializable{
      private static final long serialVersionUID = 369285298572941L;  //最好是显式声明ID
      public Inner inner;
     //Discription:[深度复制方法,需要对象及对象所有的对象属性都实现序列化] 
      public Outer myclone() {
          Outer outer = null;
          try { // 将该对象序列化成流,因为写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。所以利用这个特性可以实现对象的深拷贝
              ByteArrayOutputStream baos = new ByteArrayOutputStream();
              ObjectOutputStream oos = new ObjectOutputStream(baos);
              oos.writeObject(this);
          // 将流序列化成对象
              ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
              ObjectInputStream ois = new ObjectInputStream(bais);
              outer = (Outer) ois.readObject();
          } catch (IOException e) {
              e.printStackTrace();
          } catch (ClassNotFoundException e) {
              e.printStackTrace();
          }
          return outer;
      }
    }

    Inner也必须实现Serializable,否则无法序列化:

    public class Inner implements Serializable{
      private static final long serialVersionUID = 872390113109L; //最好是显式声明ID
      public String name = "";
    
      public Inner(String name) {
          this.name = name;
      }
    
      @Override
      public String toString() {
          return "Inner的name值为:" + name;
      }
    }

    这样也能使两个对象在内存空间内完全独立存在,互不影响对方的值。

    总结

    实现对象克隆有两种方式:

      1). 实现Cloneable接口并重写Object类中的clone()方法;

      2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。

    注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object类的clone方法克隆对象。让问题在编译的时候暴露出来总是优于把问题留到运行时。

  • 相关阅读:
    Unity
    Unity
    Unity
    Unity
    Unity项目
    Unity
    UnityShader
    Unity
    【数据结构与算法】链表——奇偶链表
    【数据结构与算法】链表——分隔链表
  • 原文地址:https://www.cnblogs.com/myseries/p/10152704.html
Copyright © 2020-2023  润新知