浅拷贝
首先创建两个类,方便理解浅拷贝
@Data class Student implements Cloneable{ //年龄和名字是基本属性 private int age; private String name; //书包是引用属性 private Bag bag; public Student(int age, String name, Bag bag) { this.age = age; this.name = name; this.bag = bag; } @Override public String toString() { return "age=" + age + ", name='" + name + ", bag=" + bag; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
@Data class Bag { private String color; private int price; public Bag(String color, int price) { this.color = color; this.price = price; } @Override public String toString() { return "color='" + color + ", price=" + price; } }
Cloneable 接口只是一个标记接口(没属性和方法):
public interface Cloneable { }
标记接口的作用其实很简单,用来表示某个功能在执行的时候是合法的。
如果不实现Cloneable接口直接重写并调用clone()方法,会抛出 CloneNotSupportedException 异常。
测试类
class TestClone { public static void main(String[] args) throws CloneNotSupportedException { Student student1 = new Student(18, "张三", new Bag("红",100)); Student student2 = (Student) student1.clone(); System.out.println("浅拷贝后:"); System.out.println("student1:" + student1); System.out.println("student2:" + student2); //修改非引用类型属性name student2.setName("李四"); //修改引用类型属性bag Bag bag = student2.getBag(); bag.setColor("蓝"); bag.setPrice(200); System.out.println("修改了 student2 的 name 和 bag 后:"); System.out.println("student1:" + student1); System.out.println("student2:" + student2); } } //打印结果 浅拷贝后: student1:age=18, name='张三, bag=color='红, price=100 student2:age=18, name='张三, bag=color='红, price=100 修改了 student2 的 name 和 bag 后: student1:age=18, name='张三, bag=color='蓝, price=200 student2:age=18, name='李四, bag=color='蓝, price=200
可以看得出,浅拷贝后:
修改了student2的非引用类型属性name,student1的name并不会跟着改变
但修改了student2的引用类型属性bag,student1的bag跟着改变了
说明浅拷贝克隆的对象中,引用类型的字段指向的是同一个,当改变任何一个对象,另外一个对象也会随之改变。
深拷贝
深拷贝和浅拷贝不同的,深拷贝中的引用类型字段也会克隆一份,当改变任何一个对象,另外一个对象不会随之改变。
例子
@Data class Bag implements Cloneable { private String color; private int price; public Bag(String color, int price) { this.color = color; this.price = price; } @Override public String toString() { return "color='" + color + ", price=" + price; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
注意,此时的 Bag 类和浅拷贝时不同,重写了 clone() 方法,并实现了 Cloneable 接口。为的就是深拷贝的时候也能够克隆该字段。
@Data class Student implements Cloneable{ //年龄和名字是基本属性 private int age; private String name; //书包是引用属性 private Bag bag; public Student(int age, String name, Bag bag) { this.age = age; this.name = name; this.bag = bag; } @Override public String toString() { return "age=" + age + ", name='" + name + ", bag=" + bag; } @Override protected Object clone() throws CloneNotSupportedException { Student s = (Student) super.clone(); s.setBag((Bag) s.getBag().clone()); return s; } }
注意,此时 Student 类也与之前的不同,clone() 方法当中,不再只调用 Object 的 clone() 方法对 Student 进行克隆了,还对 Bag 也进行了克隆。
来看测试类
class TestClone { public static void main(String[] args) throws CloneNotSupportedException { Student student1 = new Student(18, "张三", new Bag("红",100)); Student student2 = (Student) student1.clone(); System.out.println("深拷贝后:"); System.out.println("student1:" + student1); System.out.println("student2:" + student2); //修改非引用类型属性name student2.setName("李四"); //修改引用类型属性bag Bag bag = student2.getBag(); bag.setColor("蓝"); bag.setPrice(200); System.out.println("修改了 student2 的 name 和 bag 后:"); System.out.println("student1:" + student1); System.out.println("student2:" + student2); } } //这个测试类和之前的浅拷贝的测试类一样,但运行结果是不同的。 深拷贝后: student1:age=18, name='张三, bag=color='红, price=100 student2:age=18, name='张三, bag=color='红, price=100 修改了 student2 的 name 和 bag 后: student1:age=18, name='张三, bag=color='红, price=100 student2:age=18, name='李四, bag=color='蓝, price=200
不只是 student1 和 student2 是不同的对象,它们中的 bag 也是不同的对象。所以,改变了 student2 中的 bag 并不会影响到 student1。
不过,通过 clone() 方法实现的深拷贝比较笨重,因为要将所有的引用类型都重写 clone() 方法。
更好的方法是利用序列化
序列化
序列化是将对象写入流中,而反序列化是将对象从流中读取出来。写入流中的对象就是对原始对象的拷贝。需要注意的是,每个要序列化的类都要实现 Serializable 接口,该接口和 Cloneable 接口类似,都是标记型接口。
来看例子
@Data class Bag implements Serializable { private String color; private int price; public Bag(String color, int price) { this.color = color; this.price = price; } @Override public String toString() { return "color='" + color + ", price=" + price; } }
Bag 需要实现 Serializable 接口
@Data class Student implements Serializable { //年龄和名字是基本属性 private int age; private String name; //书包是引用属性 private Bag bag; public Student(int age, String name, Bag bag) { this.age = age; this.name = name; this.bag = bag; } @Override public String toString() { return "age=" + age + ", name='" + name + ", bag=" + bag; } //使用序列化拷贝 public Object serializeClone() throws IOException, ClassNotFoundException { // 序列化 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); // 反序列化 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); } }
Student 类也需要实现 Serializable 接口,并且在该类中,增加了一个 serializeClone() 的方法,利用 OutputStream 进行序列化,InputStream 进行反序列化,这样就实现了深拷贝。
来看示例
class TestClone { public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException { Student student1 = new Student(18, "张三", new Bag("红",100)); Student student2 = (Student) student1.serializeClone(); System.out.println("浅拷贝后:"); System.out.println("student1:" + student1); System.out.println("student2:" + student2); //修改非引用类型属性name student2.setName("李四"); //修改引用类型属性bag Bag bag = student2.getBag(); bag.setColor("蓝"); bag.setPrice(200); System.out.println("修改了 student2 的 name 和 bag 后:"); System.out.println("student1:" + student1); System.out.println("student2:" + student2); } } //与之前测试类不同的是,调用了 serializeClone() 方法。 浅拷贝后: student1:age=18, name='张三, bag=color='红, price=100 student2:age=18, name='张三, bag=color='红, price=100 修改了 student2 的 name 和 bag 后: student1:age=18, name='张三, bag=color='红, price=100 student2:age=18, name='李四, bag=color='蓝, price=200
测试结果和之前用 clone() 方法实现的深拷贝一样。
clone() 方法同时是一个本地(native)方法,它的具体实现会交给 HotSpot 虚拟机,那就意味着虚拟机在运行该方法的时候,会将其替换为更高效的 C/C++ 代码,进而调用操作系统去完成对象的克隆工作。
需要注意,由于是序列化涉及到输入流和输出流的读写,在性能上要比 HotSpot 虚拟机实现的 clone() 方法差很多。