java 实现对象克隆

来源:互联网 发布:剑灵捏人详细数据 编辑:程序博客网 时间:2024/06/05 00:19

1、为什么要克隆

在java中,复制一个变量很容易

int a = 5;  int b = a;  

不仅仅是int类型,其它七种原始数据类型(boolean,char,byte,short,float,double.long)同样适用于该类情况。
但是如果我们需要复制一个对象,使用变量的”=”符号复制就不正确了,除了在函数传值的时候是”引用传递”,在任何用”=”向对象变量赋值的时候都是”引用传递”。

class Student {      private int number;      public int getNumber() {          return number;      }      public void setNumber(int number) {          this.number = number;      }  }  public class Test {      public static void main(String args[]) {          Student stu1 = new Student();          stu1.setNumber(12345);          Student stu2 = stu1;          System.out.println("学生1:" + stu1.getNumber());          System.out.println("学生2:" + stu2.getNumber());      }  }

结果:

学生1:12345  学生2:12345  

这里我们自定义了一个学生类,该类只有一个number字段。
我们新建了一个学生实例,然后将该值赋值给stu2实例。(Student stu2 = stu1;)
我们试着改变stu2实例的number字段,再打印结果看看:

stu2.setNumber(54321);  System.out.println("学生1:" + stu1.getNumber());  System.out.println("学生2:" + stu2.getNumber());  

结果:

学生1:54321  学生2:54321  

原因出在(stu2 = stu1) 这一句。该语句的作用是将stu1的引用赋值给stu2,
这样,stu1和stu2指向内存堆中同一个对象。如图:
这里写图片描述
克隆的对象可能包含一些已经修改过的属性,而new出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠clone方法了。
所以我们需要能够实现对于输入的实参进行了一份拷贝,若方法参数为基本类型,则在栈内存中开辟新的空间,所有的方法体内部的操作都是针对这个拷贝的操作,并不会影响原来输入实参的值 。若方法参数为引用类型,该拷贝与输入实参指向了同一个对象,方法体内部对于对象的操作,都是针对的同一个对象。

2、如何实现克隆

两种不同的克隆方法,浅克隆(ShallowClone)和深克隆(DeepClone)。
在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制。
有两种方式:
1). 实现Cloneable接口并重写Object类中的clone()方法;
2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆;

2.1、clone()方法

/*Creates and returns a copy of this object. The precise meaning of "copy" may depend on the class of the object.The general intent is that, for any object x, the expression:1) x.clone() != x will be true2) x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements.3) x.clone().equals(x) will be true, this is not an absolute requirement.*/protected native Object clone() throws CloneNotSupportedException;

仔细一看,它还是一个native方法,大家都知道native方法是非Java语言实现的代码,供Java程序调用的,因为Java程序是运行在JVM虚拟机上面的,要想访问到比较底层的与操作系统相关的就没办法了,只能由靠近操作系统的语言来实现。
第一次声明保证克隆对象将有单独的内存地址分配。
第二次声明表明,原始和克隆的对象应该具有相同的类类型,但它不是强制性的。
第三声明表明,原始和克隆的对象应该是平等的equals()方法使用,但它不是强制性的。
因为每个类直接或间接的父类都是Object,因此它们都含有clone()方法,但是因为该方法是protected,所以都不能在类外进行访问。
要想对一个对象进行复制,就需要对clone方法覆盖。
实现clone方法的步骤
(1)实现Cloneable接口
(2)重载Object类中的clone()方法,重载时需定义为public
(3)在重载方法中,调用super.clone()

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

解释:
(1)clone()方法是定义在java.lang.Object类中,该方法是一个protected的方法,所以重载时要把clone()方法的属性设置为public,这样其它类才能调用这个clone类的clone()方法
(2)实现Cloneable接口:Cloneable接口是不包含任何方法的!其实这个接口仅仅是一个标志,而且这个标志也仅仅是针对Object类中clone()方法的,如果clone类没有实现Cloneable接口,并调用了Object的clone()方法(也就是调用了super.Clone()方法),那么Object的clone()方法就会抛出 CloneNotSupportedException异常。

2.2、通过序列化和反序列化实现克隆

import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;public class MyUtil {    private MyUtil() {        throw new AssertionError();    }    public static <T> T clone(T obj) throws Exception {        ByteArrayOutputStream bout = new ByteArrayOutputStream();        ObjectOutputStream oos = new ObjectOutputStream(bout);        oos.writeObject(obj);        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());        ObjectInputStream ois = new ObjectInputStream(bin);        return (T) ois.readObject();        // 说明:调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义        // 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这一点不同于对外部资源(如文件流)的释放    }}

测试代码:

import java.io.Serializable;/** * 人类 */class Person implements Serializable {    private static final long serialVersionUID = -9102017020286042305L;    private String name;    // 姓名    private int age;        // 年龄    private Car car;        // 座驾    public Person(String name, int age, Car car) {        this.name = name;        this.age = age;        this.car = car;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public int getAge() {        return age;    }    public void setAge(int age) {        this.age = age;    }    public Car getCar() {        return car;    }    public void setCar(Car car) {        this.car = car;    }    @Override    public String toString() {        return "Person [name=" + name + ", age=" + age + ", car=" + car + "]";    }}
/** * 小汽车类 */class Car implements Serializable {    private static final long serialVersionUID = -5713945027627603702L;    private String brand;       // 品牌    private int maxSpeed;       // 最高时速    public Car(String brand, int maxSpeed) {        this.brand = brand;        this.maxSpeed = maxSpeed;    }    public String getBrand() {        return brand;    }    public void setBrand(String brand) {        this.brand = brand;    }    public int getMaxSpeed() {        return maxSpeed;    }    public void setMaxSpeed(int maxSpeed) {        this.maxSpeed = maxSpeed;    }    @Override    public String toString() {        return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed + "]";    }}
class CloneTest {    public static void main(String[] args) {        try {            Person p1 = new Person("Hao LUO", 33, new Car("Benz", 300));            Person p2 = MyUtil.clone(p1);   // 深度克隆            p2.getCar().setBrand("BYD");            // 修改克隆的Person对象p2关联的汽车对象的品牌属性            // 原来的Person对象p1关联的汽车不会受到任何影响            // 因为在克隆Person对象时其关联的汽车对象也被克隆了            System.out.println(p1);        } catch (Exception e) {            e.printStackTrace();        }    }}

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

3、浅克隆和深克隆

3.1、浅克隆

在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。
这里写图片描述
在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。

3.2、深克隆

在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。
简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。
这里写图片描述
在Java语言中,如果需要实现深克隆,可以通过覆盖Object类的clone()方法实现,也可以通过序列化(Serialization)等方式来实现。
如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。
序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。

参考文章:
Java提高篇——对象克隆(复制)
Java clone()克隆对象

0 0