Java语言支持一种称为
对象序列化的非常通用的机制,它可以将任何对象写出到输入流中,并在之后将其读回。
一.保存和加载序列化对象
- 为了保存对象数据,首先需要打开一个
ObjectOutputStream对象。可以直接使用ObjectOutputStream的writeObject方法。对应的,将对象读回,首先需要获得一个ObjectInputStream对象,然后用readObject方法以这些对象被写出时的顺序获得它们。
try(ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("C:\\Users\\whz\\Desktop\\employee.dat"));
ObjectInputStream in = new ObjectInputStream(
new FileInputStream("C:\\Users\\whz\\Desktop\\employee.dat"))
) {
Employee xm = new Employee("小明", 30000, 1996, 3, 3);
Employee xh = new Employee("小红", 30200, 1926, 3, 3);
out.writeObject(xm);
out.writeObject(xh);
Employee employee1 = (Employee) in.readObject();
Employee employee2 = (Employee) in.readObject();
System.out.println(employee1);
System.out.println(employee2);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e){
e.printStackTrace();
}
- 需要注意的是,需要存储的对象必须实现
Serializable接口。该接口中没有任何方法,只是给对象打上一个标志。 - 有一种重要的情况需要考虑:当一个对象被多个对象共享,作为它们各自状态的一部分。为了说明这一问题,新建
Manager类,每个Manager对象都有一个Employee的助理。如下
public class Manager extends Employee {
public Manager(String name, double salary, int year, int month, int day) {
super(name, salary, year, month, day);
}
private Employee secretary;
public Employee getSecretary() {
return secretary;
}
public void setSecretary(Employee secretary) {
this.secretary = secretary;
}
@Override
public String toString() {
return super.toString()+"Manager{" +
"secretary=" + secretary +
'}';
}
}
- 这里我们不能去保存和恢复秘书对象的内存地址,因为当对象的内存地址,因为当对象被重新加载时,他可能占据的是与原来完全不同的内存地址。
- 于此不用的是,每个对象都是一个***保存的,这就是这种机制之所以称为序列化的原因。
- 保存对象时,每个对象引用都关联一个***,对于每个对象,当第一次遇到时,保存其对象数据输出流中。
- 读回对象时,对于对象输入流中的对象,在第一次遇到其***时,构建他,并使用流中数据来初始化它,然后记录这个顺序号和新对象之间的关联。
二.修改默认的序列化机制
- 某些数据域是不可以序列化的。例如,只对本地方法有意义的存储文件句柄或窗口句柄的整数值,这些信息在稍后重新加载对象或将其传送到其它机器上时都是没有用处的。
- Java拥有一种很简单的机制来防止这种域被序列化,那就是将它们标记成是
transient的。 - 在
java.awt.geom包中有大量的类都是不可序列化的,例如Point2D.Double。首先,需要将Point2D.Double标记成transient,以避免抛出NotSerializableException。
public class LabeledPoint implements Serializable {
private String label;
private transient Point2D.Double point;
}
- 在
writeObject方法中,首先通过调用defaultWriteObject方法写出对象描述符和String域label,这是ObjectOutputStream类中的一个特殊的方法,它只能在可序列化类的writeObject方法中被调用。然后,我们使用标准的DataOutput调用写出点的坐标。
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeDouble(point.getX());
out.writeDouble(point.getY());
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
double x = in.readDouble();
double y = in.readDouble();
point = new Point2D().Double(x,y);
}
- 另一个例子是
java.util.Date类,它提供了自己的readObject和writeObject方法,这些方法将日期写出为纪元(UTC时间1970年1月1日0点)开始的毫秒数。Date类有一个复杂的内部表示,为了优化查询,它存储类一个Calendar对象和一个毫秒计数值。Calendar的状态是冗余的,因此并不需要保存。 -
readObject和writeObject方法只需要保存和加载它们的数据域,而不需要关心超类数据和任何其他类的信息。 - 除了让序列化机制来保存和恢复对象数据,类还可以定义它自己的机制。为了做到这点,这个类必须实现Externalizable接口,这需要它定义两个方法:
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
- 这些方法对包括超类数据在内的整个对象的存储和恢复负全责。在写出对象时,序列化机制在输出流中仅仅只是记录该对象所属的类。在读入可外部化的类时,对象输入流将用无参构造创建一个对象,然后调用
readExternal方法。
public class Employee implements Externalizable{
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String name, double salary, int year, int month, int day){
this.name = name;
this.salary = salary;
hireDay = LocalDate.of(year, month, day);
}
public String getName(){return name;}
public double getSalary(){return salary;}
public LocalDate getHireDay(){return hireDay;}
public void raiseSalary(double byPercent){
double raise = salary * byPercent / 100;
salary += raise;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", salary=" + salary +
", hireDay=" + hireDay +
'}';
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeDouble(salary);
//从纪元算起的毫秒数
out.writeLong(hireDay.toEpochDay());
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
salary = in.readDouble();
hireDay = LocalDate.ofEpochDay(in.readLong());
}
}
三.序列化单例和类型安全的枚举
- 在序列化和反序列化时,如果目标对象是唯一的,这通常会在实现单例和类型安全的枚举时发生。
- 如果使用Java语言的enum结构,就不必担心序列化,它能够正常工作。但是,假设在维护遗留代码,其中包含下面这样的枚举
public class Orientation {
private int value;
private Orientation(int value) {
this.value = value;
}
//水平方向
public static final Orientation HORIZONTAL = new Orientation(1);
//垂直方向
public static final Orientation VERTICAL = new Orientation(2);
}
- 注意构造器是私有的,因此,不可能创建出超过
Orientation.HORIZONTAL和Orientation.VERTICAL之外的对象。特别是,可以使用==操作符来测试对象的等同性:
if(orientation == Orientation.HORIZONTAL)
- 我们写出一个Orientation类型的值,并再次将其读回:
public class OrientationTest {
public static void main(String[] args) {
Orientation orientation = Orientation.HORIZONTAL;
try(ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("Orientation.txt"));)
{
out.writeObject(orientation);
} catch (IOException e) {
e.printStackTrace();
}
try(ObjectInputStream in = new ObjectInputStream(
new FileInputStream("Orientation.txt"));)
{
Orientation saved = (Orientation) in.readObject();
System.out.println( orientation == saved );
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e){
e.printStackTrace();
}
}
}
- 事实上,
saved的值是Orientation类型的一个全新的对象,它与任何预定义的常量都不等同。即使构造器是私有的,序列化机制也可以创建新的对象。 - 为了解决这个问题,需要定义另外一种称为
readResolve的特殊序列化方法。如果定义了readResolve方法,在对象被序列化之后就会调用它,它必须返回一个对象,而该对象之后会成为readObject的返回值。
protected Object readResolve()throws ObjectStreamException{
if(value == 1) return HORIZONTAL;
if(value == 2) return VERTICAL;
//throw new ObjectStreamException();
return null;
}
- 之前的文章中有介绍过单例这一设计模式枚举实现单例,序列化对单例模式的破坏