对象克隆
对象克隆最简单的方式是:将对原对象的引用直接传给一个新的副本变量。这种方式存在很大的缺陷,两个变量中任何一个变量的改变都会影响另一个变量。
浅拷贝
利用Object
类的clone
方法,能够创建一个新的对象,并拷贝原对象的域 ,返回新对象的引用。
优点:使副本的操作与原变量的操作相对独立。
缺点:当浅克隆对象与原对象共享的子对象可变时,对可变子对象的操作仍会同时影响两个对象。
// 原对象,包含Date类子对象
Employee original = new Employee("John Public", 50000);
// 使用clone方法,两个对象共享对Date类子对象的引用
Employee copy = original.clone();
// 若修改Date类子对象,则两个对象中的Date类子对象的值都会改变
在上述情况中,由于Date
类是可变的,导致浅克隆对象和原对象之间仍然存在影响。作为替代,应当使用不可变的LocalDate
类。
Cloneable接口
实际上,Object
类中的clone
方法被声明为protected
。因此,Object
的子类只能调用clone
来克隆它们自己的对象,而不能克隆其他类的对象。
注意:Cloneable
接口是Java
提供的标记接口。标记接口不包含任何方法,其唯一的作用是允许在类型查询中使用instanceof
。
即使clone
的默认实现(浅拷贝)能够满足要求,仍然需要实现Cloneable
接口,并将clone
重新定义为public
,再调用super.clone()
。
class Employee implements Cloneable {
// 如果调用clone的类没有实现Cloneable接口,则抛出CloneNotSupportedException异常
public Employee clone() throws CloneNotSupportedException {
// 调用超类的clone方法
return (Employee) super.clone();
}
}
深拷贝
当浅拷贝无法满足对象之间的独立性时,需要建立深拷贝,即克隆对象中可变的实例域。
class Employee implements Cloneable {
// 如果调用clone的类没有实现Cloneable接口,则抛出CloneNotSupportedException异常
public Employee clone() throws CloneNotSupportedException {
// 调用超类的clone方法
Employee cloned = super.clone();
// 对可变的hireDay子对象再次建立浅拷贝
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
}
序列化
对象序列化是Java
语言支持的一种非常通用的机制,它可以将任何对象写出到输出流中,并在之后将其读回。使用对象序列化机制可能便捷地处理多态集合(例如,一个Employee
记录数组包含Manager
等子类实例)。
保存和加载序列化对象
使用ObjectOutputStream
对象的writeObject
方法保存数据对象,使用ObjectInputStream
对象的readObject
读回数据对象。使用序列化时必须实现Serializable
接口,其和Cloneable
接口很相似,没有任何方法,其他类不需要为实现它做出任何改动。
// 创建一个Employee对象和一个Manager对象
Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);
Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
// 将两个对象存储在对象输出流中
ObjectOutputStream out = new PbjectOutputStream(new FileOutputStream("employee.dat"));
out.writeObject(harry);
out.writeObject(boss);
// 将两个对象从对象输入流中恢复
ObjectInputStream in = new ObjectInputStream(new FileInputStream("employee.dat"));
Employee e1 = (Employee) in.readObject();
Employee e2 = (Employee) in.readObject();
对象序列化工作机制如下:
- 每一个对象引用关联一个序列号。
- 第一次遇到每个对象时,保存其对象数据到输出流中。
- 如果某个对象之前已经被保存过,那么只写出“与之前保存过的序列号为x的对象相同”。
- 读回时,过程相反:对于对象输入流中的对象,在第一次遇到其序列号时,构建它,并使用流中数据来初始化它,然后记录这个序列号和新对象之间的关联。
- 当遇到“与之前保存过的序列号x的对象相同”标记时,获取与这个序列号相关联的对象引用。
注:序列化使用序列号代替了内存地址,允许将对象集合在机器之间进行传送。
对象序列化的文件格式
注:以下(x)格式均表示字节长度。
对象序列化文件开头:魔幻数字(2) + 版本号(2)
类描述符的存储格式:
72 + 类名长度(2) + 类名
指纹(8) + 标志(1) + 数据域描述符的数量(2) + 数据域描述符 + 78 + 超类类型(无超类为70)
其中,指纹指通过对类、超类、接口、域类型和方法签名按照规范方式排序,然后应用安全散列算法SHA得到的。SHA将数据块转换为20个字节的数据包,序列化机制只使用SHA的前8个字节。
标志字节由java.io.ObjectStreamConstants
中定义的3位掩码构成:
static final byte SC_WRITE_METHOD = 1;
// class has a writeObject_method that writes additional data
static final byte SC_SERIALIZABLE = 2;
// class implements the Serializable interface
static final byte SC_EXTERNALIZABLE = 4;
// class implements the Externalizable interface
数据域描述符的格式如下:
类型编码(1) + 域名长度(2) + 域名 + 类名(若域是对象)
对象序列化的文件格式特点如下:
- 对象流输出中包含所有对象的类型和数据域。
- 每个对象都被赋予一个序列号。
- 相同对象的重复出现将被存储为对这个对象的序列号的引用。
修改默认的序列化机制
对于不可序列化的数据域,如只对本地方法有意义的存储文件句柄值等,序列化机制将其标记为transient
。不可序列化的类的域也需要标记为transient
。瞬时的域在对象序列化时被跳过。
在可序列化的类中可以定义writeObject
方法和readObject
方法:
private void writeObject(ObjectOutputStream out)
throws IOException;
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException;
定义上述方法的类在序列化时会调用这两个方法对其数据域进行序列化。
由于writeObject
和readObject
方法不关心超类数据和任何其他类的信息,若要对整个对象的存储和恢复负责,就要实现Externalizable
接口,定义writeExternal
方法和readExternal
方法:
public void writeExternal(ObjectOutputStream out)
throws IOException;
public void readExternal(ObjectInputStream in)
throws IOException, ClassNoutFoundException;
注意:writeObject
和readObject
方法是私有的,且只能被序列化机制调用。而writeExternal
和readExternal
方法是共有的,且readExternal
方法甚至潜在地允许修改现有对象的状态。
使用序列化技术克隆对象
只需将对象序列化到输出流中,然后将其读回,就能产生现有对象的深拷贝。在此过程中,可以使用ByteArrayOutputStream
方法将数据保存到字节数组中。如下修改clone
方法:
class SerialCloneable implements Cloneable, Serializable {
public Object clone() throws CloneNotSupportedException {
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try (ObjectOutputStream = new ObjectOutputStream(bout)) {
out.writeObject(this);
}
try (InputSteam bin = new ByteArrayInputStream(bout.toByteArray())) {
ObjectInputStream in = new ObjectInputStream(bin);
return in.readObject();
}
catch (IOException | ClassNotFoundException e) {
CloneNotSupportedException e2 = new CloneNotSupportException();
e2.initCause(e);
throw e2;
}
}
}
}
需要克隆的类只需继承SerialCloneable
类即可。