Java核心技术:输入与输出——对象输入/输出流与序列化


对象序列化可以将任何对象写出到输出流中,并在之后将其读回。

保存和加载序列化对象

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

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

可以直接调用ObjectOutputStreamwriteObject方法:

Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);
Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
out.writeObject(harry);
out.writeObject(boss);

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

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

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

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

这些类必须实现Serializable接口:

class Employee implements Serializable { ... }

Serializable接口没有任何方法,因此不需要对这些类做任何改动。

package java.io;
public class ObjectOutputStream
    extends OutputStream implements ObjectOutput, ObjectStreamConstants
{
    protected ObjectOutputStream() throws IOException, SecurityException {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
        bout = null;
        handles = null;
        subs = null;
        enableOverride = true;
        debugInfoStack = null;
    }
    public ObjectOutputStream(OutputStream out) throws IOException {
        verifySubclass();
        bout = new BlockDataOutputStream(out);
        handles = new HandleTable(10, (float) 3.00);
        subs = new ReplaceTable(10, (float) 3.00);
        enableOverride = false;
        writeStreamHeader();
        bout.setBlockDataMode(true);
        if (extendedDebugInfo) {
            debugInfoStack = new DebugTraceInfoStack();
        } else {
            debugInfoStack = null;
        }
    }
    public final void writeObject(Object obj) throws IOException {
        if (enableOverride) {
            writeObjectOverride(obj);
            return;
        }
        try {
            writeObject0(obj, false);
        } catch (IOException ex) {
            if (depth == 0) {
                writeFatalException(ex);
            }
            throw ex;
        }
    }
    private void writeObject0(Object obj, boolean unshared)
        throws IOException
    {
        boolean oldMode = bout.setBlockDataMode(false);
        depth++;
        try {
            // handle previously written and non-replaceable objects
            int h;
            if ((obj = subs.lookup(obj)) == null) {
                writeNull();
                return;
            } else if (!unshared && (h = handles.lookup(obj)) != -1) {
                writeHandle(h);
                return;
            } else if (obj instanceof Class) {
                writeClass((Class) obj, unshared);
                return;
            } else if (obj instanceof ObjectStreamClass) {
                writeClassDesc((ObjectStreamClass) obj, unshared);
                return;
            }

            // check for replacement object
            Object orig = obj;
            Class<?> cl = obj.getClass();
            ObjectStreamClass desc;
            for (;;) {
                // REMIND: skip this check for strings/arrays?
                Class<?> repCl;
                desc = ObjectStreamClass.lookup(cl, true);
                if (!desc.hasWriteReplaceMethod() ||
                    (obj = desc.invokeWriteReplace(obj)) == null ||
                    (repCl = obj.getClass()) == cl)
                {
                    break;
                }
                cl = repCl;
            }
            if (enableReplace) {
                Object rep = replaceObject(obj);
                if (rep != obj && rep != null) {
                    cl = rep.getClass();
                    desc = ObjectStreamClass.lookup(cl, true);
                }
                obj = rep;
            }

            // if object replaced, run through original checks a second time
            if (obj != orig) {
                subs.assign(orig, obj);
                if (obj == null) {
                    writeNull();
                    return;
                } else if (!unshared && (h = handles.lookup(obj)) != -1) {
                    writeHandle(h);
                    return;
                } else if (obj instanceof Class) {
                    writeClass((Class) obj, unshared);
                    return;
                } else if (obj instanceof ObjectStreamClass) {
                    writeClassDesc((ObjectStreamClass) obj, unshared);
                    return;
                }
            }

            // remaining cases
            if (obj instanceof String) {
                writeString((String) obj, unshared);
            } else if (cl.isArray()) {
                writeArray(obj, desc, unshared);
            } else if (obj instanceof Enum) {
                writeEnum((Enum<?>) obj, desc, unshared);
            } else if (obj instanceof Serializable) {
                writeOrdinaryObject(obj, desc, unshared);
            } else {
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }
        } finally {
            depth--;
            bout.setBlockDataMode(oldMode);
        }
    }
}

考虑当一个对象被多个对象共享,作为它们各自状态的一部分时,会发送什么?
例如:

class Manager extends Employee
{
    private Employee secretary;
    ...
}
harry = new Employee("Harry Hacker", ...);
Manager carl = new Manager("Carl Crcker", ...);
carl.setSecretary(harry);
Manager tony = new Manager("Tony Tester", ...);
tony.setSecretary(harry);

每个对象都是用一个序列号保存的,这就是这种机制之所以成为对象序列化的原有。其算法如下:

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

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

  • 对于对象输入流中的对象,在第一次遇到其序号时,构建它,并使用流中数据来初始化它,然后记录这个顺序号和新对象之间的关联。
  • 当遇到“与之前保存过的序列号为x的对象相同”标记时,获取与这个顺序号相关联的对象引用。
public class ObjectStreamTest {
    public static void main(String[] args) {
        Employee harry = new Employee("Harry Hacker", 5000, 1989, 10, 1);
        Manager carl = new Manager("Carl Cracker", 8000, 1987, 12, 15);
        carl.setSecretary(harry);
        Manager tony = new Manager("Tony Tester", 40000, 1990, 3, 15);
        tony.setSecretary(harry);

        Employee[] staff = new Employee[3];
        staff[0] = carl;
        staff[1] = harry;
        staff[2] = tony;

        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.dat"))){
            out.writeObject(staff);
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }


        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("employee.dat"))){
            Employee[] newStaff = (Employee[]) in.readObject();
            for (Employee employee : newStaff) {
                System.out.println(employee);
                if (employee instanceof Manager) {
                    System.out.println(((Manager) employee).getSecretary());
                }
            }
        } catch (IOException ioException) {
            ioException.printStackTrace();
        } catch (ClassNotFoundException classNotFoundException) {
            classNotFoundException.printStackTrace();
        }
    }
}

class Employee implements Serializable {
    private String name;
    private Integer salary;
    private Integer year;
    private Integer month;
    private Integer day;

    public Employee(String name, Integer salary, Integer year, Integer month, Integer day) {
        this.name = name;
        this.salary = salary;
        this.year = year;
        this.month = month;
        this.day = day;
    }
}

class Manager extends Employee {
    private Employee secretary;

    public Manager(String name, Integer salary, Integer year, Integer month, Integer day) {
        super(name, salary, year, month, day);
    }

    public Employee getSecretary() {
        return secretary;
    }

    public void setSecretary(Employee secretary) {
        this.secretary = secretary;
    }
}

执行结果打印如下:

me.aodi.object.Manager@13fee20c
me.aodi.object.Employee@4e04a765
me.aodi.object.Employee@4e04a765
me.aodi.object.Manager@783e6358
me.aodi.object.Employee@4e04a765

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

每个文件都是以AC ED这两个字节的"魔幻数字"开始,后面紧跟着对象序列化格式的版本号,目前是00 05,然后是它包含的对象序列,其顺序即它们存储的顺序。


字符串对象被存为:74 两字节表示的字符串长度 所有字符


当存储一个对象时,这个对象所属的类也必须存储。这个类的描述包含:

  • 类名
  • 序列化的版本唯一的ID,它是数据域类型和方法签名的指纹。
  • 描述序列化方法的标志集。
  • 对数据域的描述

类标识符存储方式:72 2字节的类名长度 类名 8字节长的指纹 1字节长的标志 2字节长的数据域描述符 78(结束标记) 超类类型(没有就是70)

标志字节是由ObjectStreamConstants中定义的3位掩码构成的:

/******************************************************/
/* Bit masks for ObjectStreamClass flag.*/

/**
 * 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 Externalizable data
 * written in Block Data mode.
 * Added for PROTOCOL_VERSION_2.
 *
 * @see #PROTOCOL_VERSION_2
 * @since 1.2
 */
final static byte SC_BLOCK_DATA = 0x08;

/**
 * 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;

/**
 * Bit mask for ObjectStreamClass flag. Indicates class is an enum type.
 * @since 1.5
 */
final static byte SC_ENUM = 0x10;

我们写的类实现了Serializable接口,其标志值为02。


数据域描述符格式如下:

  • 1字节长的类型编码
  • 2字节长的域名成都
  • 域名
  • 类名(如果域是对象)

(更多对象序列化格式的内容参见《Java核心技术 卷2》)
ObjectStreamTest示例中生成的employee.dat以二进制方式打开如下:
employee.dat
图中第三个红框的75标志着保存的是一个数组,类标志72,字符串标志74,数组标志75都由ObjectStreamConstants中定义:

/* Each item in the stream is preceded by a tag
   */

  /**
   * First tag value.
   */
  final static byte TC_BASE = 0x70;

  /**
   * Null object reference.
   */
  final static byte TC_NULL =         (byte)0x70;

  /**
   * Reference to an object already written into the stream.
   */
  final static byte TC_REFERENCE =    (byte)0x71;

  /**
   * new Class Descriptor.
   */
  final static byte TC_CLASSDESC =    (byte)0x72;

  /**
   * new Object.
   */
  final static byte TC_OBJECT =       (byte)0x73;

  /**
   * new String.
   */
  final static byte TC_STRING =       (byte)0x74;

  /**
   * new Array.
   */
  final static byte TC_ARRAY =        (byte)0x75;

  /**
   * Reference to Class.
   */
  final static byte TC_CLASS =        (byte)0x76;

应该记住:

  • 对象流输出中包含所有对象的类型和数据域。
  • 每个对象都被赋予一个序列号。
  • 相同对象的重复出现将被存储为对这个对象的序列号的引用。

修改默认的序列化机制

防止某些数据域被序列化,就将它们标记成是transient的。瞬时的域在对象被序列化时总是被跳过的。

可序列化的类可以定义具有下列签名的方法:

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

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


下面是一个典型示例。Point2D.Double是一个不可序列化类,假设想要序列化一个存储了一个String和一个Point2D.DoubleLabledPoint类。

  • 首选,需要将Point2D.Double标记成transient,以避免抛出NotSerializableException
public class LabledPoint implements Serializable
{
	private String lable;
	private transient Point2D.Double point;
	...
}
  • writeObject方法中,首先通过调用defaultWriteObject方法写出对象描述符和Stringlabel。然后,使用标准的DataOutput调用写出点的坐标。
private void writeObject(ObjectOutputStream out)
{
	out.defaultWriteObject();
	out.writeDouble(point.getX());
	out.writeDouble(point.getY());
}

defaultWriteObjectObjectOutputStream类中一个特殊的方法,只能在可序列化类的writeObject中调用

  • readObject方法中,反过来执行上述过程
private void readObject(ObjectInputStream in) throws IOException
{
	in.defaultReadObject();
	double x = in.readDouble();
	double y = in.readDouble();
	point = new Point2D.Double(x, y);
}

除了让序列化机制来保存和恢复对象数据,类还可以通过实现Externalizable定义它自己的机制。需要定义两个方法:

public interface Externalizable extends java.io.Serializable {
	void writeExternal(ObjectOutput out) throws IOException;
	void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

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

class ExtEmployee implements Externalizable {
    private String name;
    private Double salary;
    private LocalDate hireDay;

    public ExtEmployee(String name, Double salary, Integer year, Integer month, Integer day) {
        this.name = name;
        this.salary = salary;
        this.hireDay = LocalDate.of(year, month, day);
    }

    @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
{
	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; }
}

当类型安全的枚举实现Serializable接口时,此时,默认的序列化机制是不适用的。

Orientation original = Orientation.HORIZONTAL;
OjbectOutputStream out = ...;
out.write(original);
out.close();

ObjectInputStream in = ...;
Orientation saved = (Orientation) in.read();

现在,下面的测试将失败。

if (saved == Orientation.HORIZONTAL) ...

为了解决这个问题,需要定义另外一中成为readResolve的特殊序列化方法。**如果定义了readResolve方法,在对象被序列化之后就会调用它。**它必须返回一个对象,而该对象之后会成为readObject的返回值。

protected Object readResolve() throws ObjectStreamException
{
	if (value == 1) return Orientation.HORIZONTAL;
	if (value == 2) return Orientation.VERTICAL;
	throw new OjbectStreamException(); // this shouldn't happen

请记住向遗留代码中所有类型安全的枚举以及向所有支持单例设计模式的类中添加readResolve方法

版本管理

类的定义发生了变化,它的SHA指纹也会跟着变化,而对象输入流将拒绝读入具有不同指纹的对象。可以使用JDK的单机程序serialver来获得指纹。

  • 运行下面命令将会打印出Employee类的序列号
    serialver me.aodi.object.Manager
  • 运行serialver程序时添加-show选项,程序会产生下面图形化对话框
    serilver -show
    补充:这个值可以通过java.io.ObjectStreamClass#getSerialVersionUIDapi获取:
    public void testGetSerialVersionUID() {
       ObjectStreamClass lookup = ObjectStreamClass.lookup(Employee.class);
        long serialVersionUID = lookup.getSerialVersionUID();
        System.out.println(serialVersionUID);
    }
    

为了表名类对其早期版本保持兼容,这个类的所有较新的版本都必须把serialVersionUID常量定义为与最初版本的指纹相同。

class Employee implements Serializable {
    private static final long serialVersionUID = -3120763713420536245L;
    ...
}

如果一个类具有名为serialVersionUID的静态数据成员,它就不再需要人工地计算其指纹,而只需直接使用这个值。
一旦这个静态数据成员被置于某个类的内部,那么序列化系统就可以读入这个类的对象的不同版本。
如果这个类只有方法产生了变化,那么再读入新对象数据时是不会有任何问题的。
但如果数据域发送了变化,那么就可能有问题。

对象输入流会将这个类当前版本的数据域与被序列化的版本中的数据域进行比较,

  • 如果数据域之间的名字匹配而类型不匹配,那么对象输入流不会尝试将以各种类型转换成另一种类型;
  • 如果被序列化对象具有在当前版本没有的数据域,那么对象输入流会忽略这些额外数据;
  • 如果当前版本具有在被序列化的对象中所没有的数据域,那么这些新添加的域将被设置成它们的默认值(如果是对象则是null,如果是数字则是0,如果是boolean值则是false)。

为克隆使用序列化

序列化提供了一种克隆对象的简便途径,只要对应的类是可序列化即可。做法很简单:直接将对象序列化到输出流中,然后将其读回。这样产生的新对象是对现有对象的一个深拷贝。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值