前言
编码过程当中经常会碰到将一个对象传递给另一个对象,Java中对基本类型变量采用的是值传递,想要复制一个基本类型的变量,很简单,比如:
int a = 9;
int b = a;
其它基本类型的数据(如byte、short、long、float等)也可以通过同样的方法进行复制。
但要复制一个对象的话,情况就有些复杂了。
引用复制
记得以前要复制一个对象曾经这样写过
定义人员类
public class Person {
private String name;
//省略无参构造、带参构造,getter和setter方法
}
定义测试类
public class ObjectCopyDemo {
public static void main(String[] args) {
Person person1 = new Person();
person1.setName("成颖");
Person person2 = person1;
System.out.println(person1.getName());
System.out.println(person2.getName());
}
}
打印结果
成颖
成颖
这真的是复制了一个对象吗,当然不是,在测试类中加入下面几句
System.out.println(person1 == person2);
person1.setName("麦青");
System.out.println(person1.getName());
System.out.println(person2.getName());
打印结果为
true
麦青
麦青
第一句打印结果为true,这说明person1和person2的引用地址也相同,修改了person1的名字,person2的名字也会随之改变,这是因为person1和person2指向了内存中的同一个地址。这样的写法属于引用复制,并不算对象复制。
Java不能通过简单的赋值解决对象的复制问题,那么怎样才能复制一个对象呢。
对象复制之浅复制
定义
复制对象的所有变量都有与被复制对象相同的值,而所有对其他对象的引用仍然指向原来的对象,即对象的浅复制只会复制对象当中基本类型的变量,不会复制对象中引用类型的变量,里面的引用类型的变量会在被复制的对象和它的副本之间共享。
简而言之,浅复制仅复制要复制的对象,而不会复制对象当中引用的其他对象。
clone()方法
万类之父Object,它有11个方法,其中有两个是protected的,有一个就是clone()方法,该方法的签名是:
protected native Object clone() throws CloneNotSupportException;
clone()方法将对象复制了一份并返回给调用者。
对象复制方法的过程为:
步骤一:被复制的对象所在的类必须实现Cloneable接口(否则在调用clone方法过程中会抛出CloneNotSupportException异常),我们在这里将实现Cloneable接口的类称为Clone类。
该接口为标记接口(不含任何方法),这个标记也仅仅是针对Object类中的clone()方法的。
步骤二:重写clone()方法,将其修饰符设为public。
因为所有的Java类都直接或间接继承自Object类,因此所有的类都含有clone()方法。我们需要在Clone类之外的其他类中调用clone()方法复制该类的对象,所以我们应该重写clone()方法,将其修饰符改为public。
步骤三:方法中调用super.clone()方法得到要复制的对象。
无论Clone类的继承结构是怎样的,super.clone()都直接或间接调用了java.lang.Object类的clone()方法。
在运行时刻,Object类中的clone()方法识别出你要复制的是哪一个对象,然后为该对象分配内存空间,并进行对象的复制,然后将原对象的属性、属性值和方法一一复制到新对象的存储空间中。
一般而言,clone()方法满足:
对任何对象x,x.clone() != x;//复制对象与原对象不是同一个对象
对任何对象,都有x.clone().getClass() == x.getClass();//复制对象与原对象类型一样
若对象x所在的类没有重写equals()方法,那么应当满足x.equals(x.clone());//
复制对象与原对象除引用地址不同外,其余的都相同
注:
Object类的clone()方法是一个native方法,native方法的效率一般来说都是远高于Java中的非native方法的。这也说明了为什么用Object类的clone()方法,而不是先new一个对象,然后将原对象中的属性一一赋值给新对象,尽管这也能实现对象的复制。
示例代码
改造上面的Person类,如下
public class Person implements Cloneable {
private String name;
//省略无参构造、带参构造,setter和getter方法
//重写clone()方法
public Object clone() {
Person person = null;
try {
person = (Person)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return person;
}
}
测试类中person2对象的创建改为
Person person2 = (Person)person1.clone();
此时,打印结果为
成颖
成颖
false
麦青
成颖
person1 == person2打印结果为false,这说明person1与person2在内存中指向的不是同一个地址了。
注:复制对象返回的是一个新对象,而不是一个引用。
复制对象和用new操作符新建的对象的区别是这个副本已经包含了与原来对象完全相同的属性信息,而不是对象的初始信息。
调用Object类的clone()方法产生的效果是:
先在内存中开辟一块和原对象一样的内存空间,然后原样复制原对象中的内容,包括属性,属性值和方法。对于基本数据类型的属性,这样的操作是没有问题的,但对于引用类型的变量,它们保存的仅仅是对象的引用,这就导致复制后的对象和原对象中的引用类型变量指向的是同一个对象。这种clone也被称之为"影子clone"。
上面的这种复制被称为浅复制,为什么这么说呢?
请看下面的例子
在上面例子的基础上新建一个地址类
public class Address {
private String address;
//省略无参构造、带参构造,setter和getter方法
}
Person类中加入Address属性
public class Person implements Cloneable {
private String name;
private Address address;
//省略无参构造、带参构造,setter和getter方法,clone()方法与上面的例子相同
}
测试类改为
public class ObjectCopyDemo {
public static void main(String[] args) {
Person person1 = new Person();
person1.setName("成颖");
person1.setAddress(new Address("邯郸"));
Person person2 = (Person)person1.clone();
System.out.println(person1 == person2);
System.out.println(person1.getName() + "家住" + person1.getAddress().getAddress());
System.out.println(person2.getName() + "家住" + person2.getAddress().getAddress());
person1.setName("麦青");
person1.setAddress(new Address("重庆"));
System.out.println(person1.getName() + "家住" + person1.getAddress().getAddress());
System.out.println(person2.getName() + "家住" + person2.getAddress().getAddress());
}
}
打印结果为
false
成颖家住邯郸
成颖家住邯郸
麦青家住重庆
成颖家住重庆
从打印结果可以发现,改变person1的name属性person2的name属性并没有改变,但改变person1的address属性之后person2的address属性也随之改变了。这是因为浅复制只是复制了address变量的引用。
对象复制之深复制
定义
深复制是一整个独立的对象的复制,深复制会复制所有的属性,并复制属性指向的动态分配的内存。
当对象和它所引用的对象一起复制时也就是深复制。深复制相比于浅复制速度较慢并且资源消耗较大。
简而言之,深复制就是将对象所引用的对象都复制了一遍。
示例代码
为达到真正的复制对象,而不是复制引用属性的引用,需要将Address类可复制化,并且修改Person类中的clone()方法。
Address类改造如下
public class Address implements Cloneable {
private String address;
//省略无参构造、带参构造,setter和getter方法
//重写clone方法
public Object clone() {
Address address = null;
try {
address = (Address)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return address;
}
}
Person类的clone方法改造如下
public Object clone() {
Person person = null;
try {
person = (Person)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
person.address = (Address)address.clone();
return person;
}
用同样的测试方法测试,打印结果为
false
成颖家住邯郸
成颖家住邯郸
麦青家住重庆
成颖家住邯郸
从上面的例子中可以看出,通过Object的clone()方法可实现对象的深复制。但有一个很重要的问题,那就是对于比较复杂的对象,比如上面的Person类中引用了Address类型的属性,若Address类中再引用其他类的属性,其他类再引用其他类的属性…这样层层深入,每个类都需要实现Cloneable接口,重写clone()方法,那这工作量不是太大了。
有没有更好的办法呢?当然有!那就是利用对象的序列化和反序列化实现对象的复制。
对象复制之序列化和反序列化
我们知道,可以通过流将对象保存为字节文件,这个过程叫序列化;反过来,也可以从流中读取字节文件,然后转换成对象,这个过程叫反序列化。
把对象写到流里的过程是串行化(Serialization)过程,而把对象从流中读出来是并行化(Deserialization)过程。
写在流里的其实是对象的一个副本,原对象仍然在JVM当中。
所以在Java当中复制一个对象,可以先使对象所属的类实现Serializable接口,然后将对象(实际上只是对象的一个副本)写到一个流中,再从流里读出来,便可重建对象。
但这样做的前提是对象以及对象内部所有引用到的对象都是可串行化的,否则,就需要仔细考察那些不可串行化的对象或属性可否设置成transient,从而将之排除在复制过程之外。
示例代码
重新定义Person类,让其实现Serializable接口
public class Person implements Serializable {
private static final long seriaVersionUID = 1L;
private String name;
private Address address;
//省略无参构造、带参构造,setter和getter方法
//定义clone方法
public Object cloneObject() throws Exception {
//序列化
ByteArrayOutputStream os = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(this);
//反序列化
ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
ObjectInputStream ois = new ObjectInputStream(is);
return ois.readObject;
}
}
重新定义Address类,让其也实现Serializable接口
public class Address implements Serializable {
private static final long seriaVersionUID = 1L;
private String address;
//省略无参构造、带参构造,setter和getter方法
}
重新定义测试类
public class ObjectCopyDemo {
public static void main(String[] args) throws Exception {
Person person1 = new Person();
person1.setName("成颖");
person1.setAddress(new Address("邯郸"));
Person person2 = person1.cloneObject();
System.out.println(person1 == person2);
System.out.println(person1.getName() + "家住" + person1.getAddress().getAddress());
System.out.println(person2.getName() + "家住" + person2.getAddress().getAddress());
person1.setName("麦青");
person1.setAddress(new Address("重庆"));
System.out.println(person1.getName() + "家住" + person1.getAddress().getAddress());
System.out.println(person2.getName() + "家住" + person2.getAddress().getAddress());
}
}
打印结果为
false
成颖家住邯郸
成颖家住邯郸
麦青家住重庆
成颖家住邯郸
通过打印结果可以发现,利用序列化和反序列化实现的对象复制也属于深复制。