(5) 对象流与序列化
Java支持对象序列化(object serialization),可以将任何对象写出到流中,并在之后将其读回。
(1)保存对象数据通过使用ObjectOutputStream对象
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("filename"));
(2)通过writeObject方法保存对象
Employee jack = new Employee("Jack", 5000, 1986, 10, 1); Manager boss = new Manager("Carl", 50000, 1980, 10, 1); out.writeObject(jack); out.writeObject(jack);
(3)通过ObjectInputStream读回对象
ObjectInputStream in = new ObjectInputStream(new FileOutputStream("filename"));
(4)用readObject方法以这些对象被写出的顺序获取它们
Employee e1 = (Bean)in.readObject(); Employee e2 = (Bean)in.readObject();
(5)对所有希望在对象流中存储或恢复的类需要实现Serializable接口,Serializable接口没有任何方法。
class Bean implements Serializable{...}
注意:只有写对象时才使用writeObject/readObject方法,对于基本数据类型值,需要使用writeInt/readInt这样的方法。
对象序列化算法,对象都是用一个序列号(serial number)保存的,这就是这种机制成为对象序列化的原因,用于处理一个对象被对个对象共享作为它们各自属性的一部分。
(1)每一个对象引用都关联一个序列号。
(2)每个对象,第一次遇到时,保存其对象数据到流中。
(3)如果一个对象已经保存过,那么只写出“与之前保存过的序列号为X的对象相同”。在读回对象时,整个过程是反过来的。
(4)对于流中的对象,第一次遇到其序列号时,构建它,并使用流中数据来初始化它,然后记录顺序号和新对象直接的关联。
(5)当遇到“与之前保存过的序列号为X的对象相同”标记时,获取这个顺序号相关联的对象引用。
1.对象序列化文件格式
(1)每个文件以都是以 AC ED 两个字节开始的。
(2)后面紧跟着对象序列化的版本号,目前是 00 05 (Java SE 6.0)
(3)包含的对象序列,顺序是它们的存储顺序
例如,字符串对象"string"被存为 74 00 05 string ,字符串中的Unicode字符被存储过修订过的UTF-8格式
(4)当存储一个对象时,这个对象所属的类也必须存储。
这个类的描述包含:类名;序列化的版本唯一的ID,它是数据域类型和方法签名的指纹;描述序列化方法的表示集;对数据域的描述。
指纹是通过对类、超类、接口、域类型和方法签名按照规范方法排序,然后将安全散列算法(SHA)应用于这些数据而获得的。
SHA是一种可以为较大的信息块提供指纹的快速算法,不论最初的数据块尺寸有多大,这种指纹总是20个字节的数据包。它是通过在数据上执行一个灵巧的位操作序列而创建的。但是序列化机制只是用了SHA码前8个字节。即使这样,当类的数据域方法发生变化时,其指纹跟着变化的可能性还是非常大。
在读入一个对象时,会拿起指纹与它所属的类的当前指纹进行对比,如果他们不匹配,那么就说明这个类的定义在该对象被写出之后发生过变化,因此会产生一个异常。
(5)类标识符存储过程
72
2字节长的类名
类名
8字节长的指纹
1字节长的标示
2字节长的数据域描述符的计数值
数据域描述符
78 (结束标记)
超类类型 (如果没有就是70)
(6)标示字节是由在java.io.ObjectStreamConstants中定义的3位掩码构成的:
// Bit mask for ObjectStreamClass flag. Indicates a Serializable class .defines its own writeObject method.
final static byte SC_WRITE_METHOD = 0x01;
//Bit mask for ObjectStreamClass flag. Indicates class is Serializable.
final static byte SC_SERIALIZABLE = 0x02;
//Bit mask for ObjectStreamClass flag. Indicates class is Externalizable.
final static byte SC_EXTERNALIZABLE = 0x04;
可外部化的类向客户提供了可以导出其实例域的输出的读写方法,我们要写出的这些类实现了Serializable,并且其标志值为02,而可序列化的java.util.Date类定义了它自己的readObject/writeObject方法,并且其标志值为03
(6)每个数据域描述符的格式如下:
1字节长的类型编码
2字节长的域名
域名
类名(如果域是对象)
(8)其中类型编码是下列取值之一:B byte; C char; D double; F float; I int; J Long; L 对象; S short; Z boolean; [ 数组;
当类型编码为L时,域名后面紧跟域的类型。类名和域名字符串不是以字符串编码74开头的,而域类型却是。域类型使用的是与域类型稍有不同的编码机制,即本地方法使用的格式。
e.g.Employee类的薪水域被编码为
D 00 06 salary
e.g.Employee类完整的类描述符:
72 00 08 Emplyee
E6 D2 86 7D AE AC 18 1B 02 指纹和标志
00 03 实例域的数量
D 00 06 salary 实例域的类型和名字
L 00 07 hireDay 实例域的类型和名字
74 00 10 Ljava/util/Date; 实例域的类名-Date
L 00 04 name 实例域的类型和名字
74 00 12 Ljava/lang/String; 实例域的类名-String
78 结束标记
70 无超类
(9)如果在文件中再次需要相同的类描述符,可以使用一种缩写版
71 4字节长的序列号
这个序列号将引用到前面已经描述过的类描述符
对象将被存储为:
73 类描述符 对象数据
e.g.Employee对象如何存储
40 E8 6A 00 00 00 00 00 salary域的值-double
73 hireDay域的值-新对象
71 00 7E 00 08 已有的类java.util.Date
77 08 00 00 00 91 1B 4E B1 80 78 外部存储
74 00 0C Harry Hacker name域的值-String
(10)数组存储格式
75 描述符 4字节长的数组项的数量 数组项
在类描述符中的数组名的格式与本地化方法中使用的格式相同。类名以L开头,以分号结束。
(11)所有对象(包括数组和字符串)和所有类的描述符在存储到输出文件时都被赋予了一个序列号,这个数字以00 7E 00 00 开头。
e.g.上例中Date类的重复引用
71 00 7E 00 08
相同的机制还被用于对象。如果写一个对之前存储过的对象的引用,那么这个引用就会以完全相同的方式存储,即71后面跟随序列号,从上下文中可以清楚的了解这个特殊的序列引用表示的是类描述符还是对象。
(12)空引用被存储为: 70
注意:
(1)对象输出流中包含所有对象的类型和数据域。
(2)每个对象都被赋予一个序列号。
(3)相同的对象重复出现将被存储为对这个对象的序列号的引用。
2.修改默认的序列化机制
(1)transient:跳过序列化。
例如只对本地访问有意义的存储文件句柄或窗口句柄的整数值等数据域永远都不应该被序列化,或数据域属于不可序列化的类,就将标记成transient。
(2)可序列化的类可以定义具有的签名方法:序列化机制为单个类提供了一种方法,向默认的读写行为添加验证或任何其他想要的行为
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
private void writeObject(ObjectOutputStream out) throws IOException, ClassNotFoundException
ObjectInputStream.defaultReadObject()和DataOutputStream.defaultWriteObject()为特殊方法只能在可序列化类中的writeObject方法和readObject方法中调用。
之后属性不在自动序列化,而是调用这些方法。
e.g.java.awt.geom.Point2D.Double不可序列化的处理方法
public class LabeledPoint implements Serializable{ //将Point2D.Double标记成transient private transient Point2D.Double point; //先通过defaultWriteObject方法写出对象描述符和其他属性,再用标准的DataOutput调用写出坐标点 private void writeObject(ObjectOutputStream out) throws IOException, ClassNotFoundException{ out.defaultWriteObject(); out.writeDouble(point.getX()); out.writeDouble(point.getY()); } //先通过defaultWriteObject方法写出对象描述符和其他属性,再用标准的DataOutput调用写出坐标点 private void readObject(ObjectInputStream in) throws IOException{ in.defalutReadObject(); point = new Point2D.Double(out.readDouble(), out.readDouble()); } }
另一个例子是java.util.Date,它提供了自己的readObject和writeObject方法。
注意:readObject和writeObject方法只需要保护和加载他们的数据域(属性),而不需要关心超类数据和任何其他类的信息。
(3)Externalizable接口:定义类自己的机制
实现该接口必须实现两个方法
public void readExternal(ObjectInputStream in) throws IOException, ClassNotFoundException
public void writeExternal(ObjectOutputStream out) throws IOException
与readObject和writeObject方法不同,这两个方法对包括超类数据在内的整个对象的存储和恢复负全责,而序列化机制在流中仅仅是记录该对象所属的类。在读入可外部化的类时,对象流将用默认的构造器创建一个对象,然后调用readExternal方法。
e.g.Employee实现readExternal和writeExternal方法
public void readExternale(ObjectInputStream in) throws IOException{ name = s.readUTF(); salary = s.readDouble(); hireDay = new Date(s.readLong()); } public void writeExternale(ObjectOutputStream out) throws IOException, ClassNotFoundException{ out.writeUTF(name); out.writeDouble(salary); out.writeLong(hireDay.getTime()); }
注意:
(1)同时实现Serialziable和Externalizable接口的情况下,序列化调用readExternal和writeExternal方法,且调用readExternal和writeExternale方法必须有有效的构造。
(2)序列化效率不高的原因是虚拟机必须解析每个对象的结构,如果想要提高性能,并需要读写某个特定类的大量对象,需要使用到Externalizable接口。
(3)readObject和writeObject方法是私有的,并且只能被序列化机制调用。而readExternal和writeExternal方法是公共的。特别是,readExternal方法还潜在的允许修改现有对象的状态。
3.序列化单例和类型安全的枚举
问题描述:
序列化和反序列化对象在实现单例和类型安全的枚举时需要特别注意。
比如下面这样风格的枚举类型:
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; } }
因为构造器是私有的,所以不可能创造出超出Orientation.HORIZONTAL和Orientation.VERTICAL之外的对象
但是当写出一个Orientation,并读回后的实例是Orientation类型的一个全新对象,它与任何预定义的常量都不相同,即使构造器是似有的,序列化机制也可以创建新对象。
Orientation ori = Orientation.HORIZONTAL ObjectOutputStream out = ...; out.writeObject(ori); out.close(); ObjectInputStream in = ...; Orientation savedOri = (Orientation)in.readObject(); //此时 ori != savedOri !!!
解决办法:
readResolve的特殊序列化方法
如果定义了readResolve方法,在对象被序列化之后就会调用它,它必须返回一个对象,而该对象之后会成为readObject的返回值。在上面的情况中,readResolve方法将检查value域并返回恰当的枚举常量。
protected Object readResolve() throws ObjectStreamException{ if(value==1){ return Orientation.HORIZONTAL; }else if(value == 2){ return Orientation.VERTICAL; } return null; }
需要向遗留代码中所有类型安全的枚举以及所有支持单例设计模式的类中添加readResolve方法。
同时存在readExternal和readResolve的情况下先执行readExternale方法后执行readResolve方法。
4. 版本管理
当类可能进行修改,即SHA指纹会变化的情况下,需要读入这个类的不同版本,通过serialVersionUID实现。
e.g.public static final long serialVersionUID = -7920294076289299567L;
当存储名为serialVersionUID的静态常量之后就不再计算指纹,而是直接使用这个值。
注意:
(1)当属性名字匹配而类型不匹配的情况下,对象流不会尝试将一种类型转换成另一种类型,因为两个对象不兼容。
(2)如果流中的对象具有在当前版本中所没有的属性时,对象流会忽略这些额外的数据。
(3)如果当前版本具有在流化对象中所没有的属性,那么新添加的属性将会被设置成它们的默认值(对象 null, 数字 0, boolean false)
(4)如果需要所有的属性都初始化为非null的值,那么要在readObject方法中实现额外的代码去修订版本不兼容问题。
5.为克隆使用序列化
通过序列化对现有对象实现深拷贝(deep copy):直接将对象序列化到输出流中,然后将其读回。
package clone; import java.io.Serializable; import java.util.Date; public class Employee extends SerialCloneable{ private String name; private Double salary; private Date hireDay; public Employee() { } public Employee(String name, Double salary, Date hireDay) { this.setName(name); this.setSalary(salary); this.setHireDay(hireDay); } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getSalary() { return salary; } public void setSalary(Double salary) { this.salary = salary; } public Date getHireDay() { return hireDay; } public void setHireDay(Date hireDay) { this.hireDay = hireDay; } @Override public String toString() { return "Employee [name=" + name + ", salary=" + salary + ", hireDay=" + hireDay + "]"; } }
package clone; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class SerialCloneable implements Cloneable, Serializable{ public Object clone(){ try{ ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bout); out.writeObject(this); out.close(); ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray()); ObjectInputStream in = new ObjectInputStream(bin); Object obj = in.readObject(); in.close(); return obj; }catch(Exception e){ e.printStackTrace(); return null; } } }
package clone; import java.util.Date; public class CloneMain { public static void main(String[] args) { Employee e1 = new Employee("e1", 5000d, new Date()); Employee e2 = (Employee) e1.clone(); e2.setName("e2"); System.out.println(e1); System.out.println(e2); } }