输入与输出-2.4对象输入/输出与序列化

2.4 对象输入/输出与序列化

当需要存储相同类型的数据时,固定长度的记录格式是一个不错的选择,但是在面向对象程序中创建的对象很少全部都具有相同的类型。例如,staff数组,名义上是一个Employee记录数组,但是实际上却包含诸如Manager这样的子类实例。

Java语言支持一种称为对象序列化的非常通用的机制,他可以将任何对象写出到输出流中,并在之后将其读回。

2.4.1 保存和加载序列化实例

为了保存对象数据,首先需要打开一个ObjectOutputStream对象:

ObjectOutputStream out= new ObjectOutputStream(new FileOutputStream("employee.dat"));

为了保存对象,可以直接使用ObjectOutputStream的writerObject方法:

Employee harry = new Employee(...);
Manager boss = new Manager(...);
out.writeObject(harry);
out.writeObject(boss);

为了将这些对象读回,首先需要获得一个ObjectInputStream对象:

ObjectIntputStream in = new ObjectInputStream(new FileInputStream("employee.dat"));

用readObject方法以这些对象被写出时的顺序获得他们:

Employee e1 = (Employee) in.readObject();
Employee e2 = (Employee) in.readObject();

如果希望在对象输出流中存储或从对象输出流中恢复所有类,都应该进行一下修改,这些类必须实现Serialzable接口:

class Employee implements Serializable{...}

Serializable接口没有任何方法。与Cloneable接口很相似,但是,为了使类可克隆,需要覆盖Object类中的clone方法,而为了使类可序列化,不需要做任何事。

在幕后,ObjectOutputStream在浏览对象所有的域,并存储它们的内容。

但是,当一个对象被多个对象共享作为他们各自状态一部分时,会发生什么呢?

假设每个经理都有一个秘书,当然,两个经理可以共用一个秘书:

class Manager extends Employee
{
	private Employee secretary;
	...
}

保存这样的对象网络是一种挑战,在这里不能去保存和恢复秘书对象的内存地址,因为当对象被重新加载时,他可能占据的是与原来完全不同的内存地址。

与此不同的是,每个对象都是用一个序列号保存的,这就是这种机制之所以称为对象序列化的原因。下面是其算法:

  • 遇到的每一个对象引用都关联一个序列号。
  • 对于每个对象,当第一次遇见时,保存其对象数据到输出流中。
  • 如果某个对象之前已经被保存过,那么只写出“与之前保存过的序列号为x的对象相同”。

在读回对象时,整个过程时反过来的:

  • 对于对象输入流中的对象,在第一次遇见其序列号时,构建它,并使用流中数据来初始化他,然后记录这个顺序号和新对象之间的关联。
  • 当遇到“与之前保存过的序列号为x的对象相同”标记时,获取与这个顺序号相关联的对象的引用。

注意:序列化另一种非常重要的应用是通过网络将对象集合传送到另一台计算机上。正如在文件中保存原生的内存地址毫无意义一样,这些地址对于在不同的处理器之间的通信也是毫无意义的。因为序列化用序列号代替了内存地址,所以它允许将对象集合从一台机器传送到另一台机器。

2.4.2 理解对象序列化的文件格式

对象序列化是以特殊的文件格式存储对象数据的,这种数据格式对于洞察对象流化的处理过程非常有益。因为其细节显得有些专业,则跳过这一节。

2.4.3 修改默认的序列化机制

某些数据域是不可以序列化的,例如,只对本地方法有意义的存储文件句柄或窗口句柄的整数值,这种信息在稍后重新加载对象文件或将其传送到其他机器上时都是没有用处的。Java有一种很简单的机制防止这种域被序列化,将他们标记成是transient的。瞬时的域在对象被序列化时总是被跳过的。

序列化机制为单个的类提供了一种方式,向默认的读写行为添加验证或任何其他方式想要的行为。可序列化的类可以定义具有下列签名的方法:

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
private void writeObject(ObjectOutputStream out) throws IOException; 

之后,数据域就再也不会被自动序列化,取而代之的是调用这些方法。

下面是一个典型的示例。在Java.awt.geom包中有大量的类是不可以序列化的,例如Point2D.Double。假设现在要序列化一个LabeledPoint类,他存储了一个String和一个Point2D.Double。

首先将Point2D.Double标记成transient,以避免抛出NotSerializableException:

Public class LabeledPoint implements Serializable
{
	private String Label;
	private transient Pont2D.Doube point;
	...
}

writeObject方法中,我们先调用defaultWriteObject方法写出对象描述符和String域label,这是ObjectOutputStream类中的一个特殊的方法,他只能在序列化类的writeObject方法中被调用:

private void writeObject(ObjectOutputStream out)trows IOException
{
	out.defaultWriteObject();
	out.writeDouble(point.getX());
	out.writeDouble(point.getY());
}

在readObject方法中,反过来执行上述过程:

private void readObject(ObjectInputStream out)trows IOException
{
	in.defaultReadObject();
	double x = in.readDouble();
	doubel y = in.readDouble();
	point = new Point2D.Double(x,y);
}

另一个例子是java.util.Date类,它提供了自己的writeObject和readObject类。Data类有一个复杂的内部表示,为了优化查询,他储存了一个Calendar对象和一个毫秒计数值。Calendar状态是冗余的,因此不需要保存。

readerObject 和 writerObject方法只需要保存和加载他们的数据域,不需要关心超类数据和任何其他类数据。

类还可以定义它自己的机制,必须实现Externalizable接口,这需要它定义两个方法:

public void readExternal(ObjectInputStream in )throws IOException, ClassNotFoundException;
public void writeExternal(ObjectOutStream out)throws IOException;

这些方法对包括超类数据在内的整个对象的存储和恢复负全责。在写出对象时,可序列化机制在输出流中仅仅只是记录该对象所属的类。在读入可外部化类时,对象输入流将用无参构造器创建一个对象,然后调用readExternal方法下面展示了如何为Employee类实现这些方法:

public void readExternal(ObjectInput s)throws IOException
{
	name = s.readUTF();
	salary = s.readDouble();
	hireDay = LocalDate.ofEpochDay(s.readLong());
}

public void writeExternal(ObjectOutput s)throws IOException
{
	s.writeUTF(name);
	s.writeDouble(salary);
	s.writeLong(hireDay.toEpochDay());
}

警告:readObject和writeObject是私有的,并且只能被序列化机制调用。readExternal和writeExternal方法是公共的。readExternal方法还潜在的允许修改现有对象的状态。

2.2.4 序列化单例和类型安全地枚举

在序列化和反序列化时,如果目标对事项是唯一的,那么需要加倍小心。

如果使用Java语言的enum结构,那么不必担心序列化,它能正常工作,但是如果包含下面这样的枚举类型:

public class Orientation
{
	public static final Orientation HORIZONTAL = new Orientation(1);
	public static final Orientation VERTICAL = new Orientation(2);
	private int value;

	private Orientation(int v) {value = v;}
}

这种风格在枚举被添加到Java语言之前是很普遍的。注意,其构造器是私有的,因此,不可能创建出超出Orientation.HORIZONTAL和Orientation.VERTICAL之外的对象。可以用==操作符来测试对象的等同性:

if(orientation == Orientation.HORIZONTAL)...

当类型安全的枚举实现Serializable接口时,必须牢记存在着一种重要的变化,此时,默认的序列化机制是不适用的。假设写出一个Orientation类型的值,并再次将其读回:

Orientation original = Orientation.HORIZONTAL;
ObjectOutputStream out = ...;
out.write(original);
out.close();
ObjectInputStream in = ...;
Orientation saved = (Orientation)in.read();
//现在,下面的测试:
if (saved == Orientation.HORIZONTAL)...

测试将失败,即使构造器是私有的,序列化机制也可以创建新的对象!

为了解决这个问题,需要定义另外一种成为readResolve的特殊序列化方法。在对象被序列化之后就会调用它。他必须返回一个对象,而该对象之后会成为readObject的返回值。在上面的情况中,readResolve方法将检查value域并返回恰当的枚举常量:

potected Object readResolve() throws ObjectStreamException
{
	if(value == 1)return Orientation.HORIZOTAL;
	if(value == 2)return Orientation.VERTICAL;
	throw new ObjectStreamException();
}

2.4.5版本管理

无论类的定义产生了什么样的变化,它的SHA指纹也会跟着变化,对象输入流将拒绝读入具有不同指纹的对象。但是如果想要对早期版本保持兼容,就必须首先获得这个类的早期版本的指纹。可以使用JDK中的单机程序serialver来获得这个数字。下面这个命令:

serialver Employee

将会打印出

Employee: static fianl long serialVersionUID:-18142398...;

如果添加-show选项,那么就会产生图形化对话框。

这个类的所有比较新的版本都必须把serialVersionUID常量定义为与最初的指纹相同。

class Employee implements Serializable
{
	...
	public static final long serialVersionUID = -18142398...;
}

如果有一个类具有名为serialVersionUID的静态数据成员,他就不需要人工的计算其指纹,而只需要直接使用这个值。

如果只有这个类的方法产生了变化,那么在读入新对象数据时是不会有任何问题的。

对象流只会考虑非瞬时非静态的数据域。如果这两个版本分数据与之间名字匹配不兼容,对象流不会尝试将一种类型转化成另一种类型。如果旧版本具有新版本没有的数据域,那么对象流会忽略这些额外的数据;如果旧版本没有新版本的某些数据域,那么新添加的域将会设置成默认值(对象:null,数字:0,boolean:false)。

但是这种处理并不是绝对安全的,将数据域设置成null并不完全安全。

2.4.6 为克隆使用序列化

序列化机制为克隆提供了一种克隆对象的简便途径,只要对应的类时可序列化的即可。做法:直接将对象序列化到输出流中,然后将其读回。这样产生对新对象是对现有对象的深拷贝。此过程不必写入文件中,可以用ByteArrayOutputStream将数据保存到字节数组中。

猜你喜欢

转载自blog.csdn.net/z036548/article/details/84496501