对象序列化
1 前言
本文会通过简单的例子介绍如何对实现了 Serializable 接口的类进行序列化和反序列化,这部分是使用 Serializable 的入门;接着会重点分析序列化步骤与反序列化步骤,这部分会分析源码,加深对原理的理解;最后会列举实际开发中使用 Serializable 接口会遇到的问题并一一进行解决,这部分对开发中会遇到的问题进行填坑。
2 正文
2.1 入门
在实际开发中,我们会遇到这样的需求:为了将数据持久化,将对象转化为字节序列保存在磁盘上,或者反过来,需要使用数据时将保存在磁盘上的文件转为对象。前者称为序列化,后者称为反序列化。
会不会有同学这样想,为什么不直接把对象存在磁盘上,而非要把对象转为字节序列呢?
这是因为在系统底层,数据的传输形式是以简单的字节序列形式传递,也就是说,在系统底层,不能识别对象,只能识别字节序列。
在 Java 中,需要类实现 Serializable 标记接口,并借助 ObjectOutputStream 和 ObjectInputStream 实现序列化与反序列化。
这里把序列化与反序列化的过程封装为工具类 SerializeUtils,代码如下:
public class SerializeUtils { public static void writeObject(String filePath, Object obj) throws IOException { FileOutputStream fos = new FileOutputStream(filePath); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(obj); oos.close(); } public static <T> T readObject(String filePath) throws IOException, ClassNotFoundException { FileInputStream fis = new FileInputStream(filePath); ObjectInputStream ois = new ObjectInputStream(fis); T result = (T) ois.readObject(); ois.close(); return result; } }对 SerializeUtils 进行简单的说明:
- writeObject(String filePath, Object obj) 方法接收两个参数,第一个参数是要写入的文件路径,第二个参数是需要序列化的对象。在方法体内,首先创建了一个 FileOutputStream 对象,文件输出流;再把 FileOutputStream 对象传递给 ObjectOutputStream 的构造器,创建 ObjectOutputStream 对象;接着,调用 oos.writeObject(obj); 把对象写入到文件中;最后关闭输出流。
- readObject(String filePath) 方法接收一个参数,表示从哪个文件读入。另外,这是一个泛型方法,方便在方法体内进行强制类型转换。在方法体内,首先创建了一个 FileInputStream 对象,文件输入流;再把 FileInputStream 对象传递给 ObjectInputStream 的构造器,创建 ObjectInputStream 对象;接着,调用 ois.readObject() 获取文件中的对象,并强转为替换了泛型类型参数的实际类型。不过,这里的泛型方法在调用时可以利用类型推断,免去了传递的类型来替换泛型类型参数的麻烦。
下面开始代码演示:
Person1 类如下:
public class Person1 { private String name; private int age; public Person1(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }这是一个简单的 Java Bean 类,包含了两个字段,一个构造方法,还有 getter/setter 方法。
声明文件路径为静态变量:
private static String filePath = "./obj.object";在 main() 方法中,开始序列化与反序列化:
SerializeUtils.writeObject(filePath, new Person1("wzc", 32)); Person1 person1 = SerializeUtils.<Person1>readObject(filePath); System.out.println(person1.getName() + ":" + person1.getAge());运行后,查看结果:
Exception in thread "main" java.io.NotSerializableException: com.java.advanced.features.io.serialize.serializable.Person1 at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184) at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348) at com.java.advanced.features.io.serialize.SerializeUtils.writeObject(SerializeUtils.java:9) at com.java.advanced.features.io.serialize.serializable.SerializableTest.test1(SerializableTest.java:301) at com.java.advanced.features.io.serialize.serializable.SerializableTest.main(SerializableTest.java:13)可以看到程序抛出了异常:NotSerializableException,这是因为 Person1 类没有实现 Serializable 接口导致的。
创建 Person2 类,就是在 Person1 的基础上实现 Serializable 接口。
public class Person2 implements Serializable { // 省略了与 Person1 类似的代码 }再次进行测试,先执行序列化的代码:
SerializeUtils.writeObject(filePath, new Person2("wzc", 32));可以看到项目的根目录会生成 obj.object 文件:如果我们使用文本编辑器打开 obj.object 文件,可以看到文件里会有一些乱码:
\AC\ED\00sr\00<com.java.advanced.features.io.serialize.serializable.Person2\00\00\00\00\00\00\00\00I\00ageL\00namet\00Ljava/lang/String;xp\00\00\00 t\00\00\00\00\00这是因为编码问题导致的,我们写入到 obj.object 文件里的是字节序列,而打开文本编辑器使用的解码格式是 UTF-8 或者其它,如果字节序列在解码格式的编码表中找不到对应的字符,就会出现乱码。
我们应该使用打开二进制文件的编辑器来查看。在 Windows 下,可以使⽤ NotePad++打开, 添加 Hex Editor 插件查看对应的⼆进制⽂件。这里我使用的是 Ubuntu 的 GHex 工具来打开:再执行反序列化的代码:
Person2 person2 = SerializeUtils.readObject(filePath); System.out.println(person2.getName() + ":" + person2.getAge());运行后,打印结果如下:
wzc:32到这里,对如何使用对实现了 Serializable 接口的类进行序列化和反序列化已经介绍完毕。
下面开始分析序列化流程和反序列化流程:
2.2 流程分析
2.2.1 序列化流程分析
创建 ObjectOutputStream 对象,写入流的头信息
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; }我们看一下 writeStreamHeader() 方法的实现:
protected void writeStreamHeader() throws IOException { bout.writeShort(STREAM_MAGIC); bout.writeShort(STREAM_VERSION); }而 STREAM_MAGIC 和 STREAM_VERSION 是 ObjectOutputStream 所实现的接口 ObjectStreamConstants 中的常量:
public interface ObjectStreamConstants { /** * Magic number that is written to the stream header. */ final static short STREAM_MAGIC = (short)0xaced; /** * Version number that is written to the stream header. */ final static short STREAM_VERSION = 5; // 省略了其他常量。。。 }再来看一下二进制文件的截图,可以对应到写入的信息:
writeObject(Object obj) 方法
public final void writeObject(Object obj) throws IOException { if (enableOverride) { // enableOverride 是 false,不会走这个分支的 writeObjectOverride(obj); return; } try { writeObject0(obj, false); // => 代码走到这里 } catch (IOException ex) { if (depth == 0) { writeFatalException(ex); } throw ex; } }writeObject0(Object obj, boolean unshared) 方法
writeObject0() 方法是 writeObject() 方法的底层实现。
private void writeObject0(Object obj, boolean unshared) throws IOException { boolean oldMode = bout.setBlockDataMode(false); try { // handle previously written and non-replaceable objects // 省略与分析无关的代码 Class<?> cl = obj.getClass(); ObjectStreamClass desc = ObjectStreamClass.lookup(cl, true); // 省略与分析无关的代码 // 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 { bout.setBlockDataMode(oldMode); } }这个方法里面的
Class<?> cl = obj.getClass(); ObjectStreamClass.lookup(cl, true)lookup() 方法会去查找并返回给定类的类描述符对象,即 ObjectStreamClass 对象。
lookup() 方法的实现思路就是先查找缓存有没有 ObjectStreamClass 对象,有则返回;没有的话,就去调用 ObjectStreamClass 的构造方法创建 ObjectStreamClass 对象。
我们不用去考虑缓存,因为我们的代码刚跑起来,哪有缓存?
我们直接去看 ObjectStreamClass 的构造方法:
private ObjectStreamClass(final Class<?> cl) { this.cl = cl; name = cl.getName(); // 类名 isProxy = Proxy.isProxyClass(cl); // 是否是代理类 isEnum = Enum.class.isAssignableFrom(cl); // 是否是枚举类 serializable = Serializable.class.isAssignableFrom(cl); // 是否实现了 Serializable 接口 externalizable = Externalizable.class.isAssignableFrom(cl); // 是否实现了 Externalizable 接口 Class<?> superCl = cl.getSuperclass(); superDesc = (superCl != null) ? lookup(superCl, false) : null; localDesc = this; if (serializable) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { if (isEnum) { suid = Long.valueOf(0); fields = NO_FIELDS; return null; } if (cl.isArray()) { fields = NO_FIELDS; return null; } suid = getDeclaredSUID(cl); // 获取 serialVersionUID 的值 try { fields = getSerialFields(cl); computeFieldOffsets(); } catch (InvalidClassException e) { serializeEx = deserializeEx = new ExceptionInfo(e.classname, e.getMessage()); fields = NO_FIELDS; } if (externalizable) { cons = getExternalizableConstructor(cl); } else { cons = getSerializableConstructor(cl); // 获取 private void writeObject(ObjectOutputStream oos) 方法的 Method 对象 writeObjectMethod = getPrivateMethod(cl, "writeObject", new Class<?>[] { ObjectOutputStream.class }, Void.TYPE); // 获取 private void readObject(ObjectInputStream ois) 方法的 Method 对象 readObjectMethod = getPrivateMethod(cl, "readObject", new Class<?>[] { ObjectInputStream.class }, Void.TYPE); readObjectNoDataMethod = getPrivateMethod( cl, "readObjectNoData", null, Void.TYPE); hasWriteObjectData = (writeObjectMethod != null); } domains = getProtectionDomains(cons, cl); // 获取 private Object writeReplace() 方法的 Method 对象 writeReplaceMethod = getInheritableMethod( cl, "writeReplace", null, Object.class); // 获取 private Object readResolve() 方法的 Method 对象 readResolveMethod = getInheritableMethod( cl, "readResolve", null, Object.class); return null; } }); } else { suid = Long.valueOf(0); fields = NO_FIELDS; } try { fieldRefl = getReflector(fields, this); } catch (InvalidClassException ex) { // field mismatches impossible when matching local fields vs. self throw new InternalError(ex); } if (deserializeEx == null) { if (isEnum) { deserializeEx = new ExceptionInfo(name, "enum type"); } else if (cons == null) { deserializeEx = new ExceptionInfo(name, "no valid constructor"); } } for (int i = 0; i < fields.length; i++) { if (fields[i].getField() == null) { defaultSerializeEx = new ExceptionInfo( name, "unmatched serializable field(s) declared"); } } initialized = true; }从上面的注释可以看到,ObjectStreamClass 类就是在序列化过程中要来描述需要序列化的对象的。
回到 writeObject0() 方法里,obj 就是 Person2 对象,它实现了 Serializable 接口,所以obj instanceof Serializable 为 true,代码会走 writeOrdinaryObject(obj, desc, unshared) 方法。
writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) 方法
这个方法的参数值为:
- Object obj, 即 Person2 对象;
- ObjectStreamClass desc,即对应于 Person2 类的类描述信息封装;
- boolean unshared,即 false。
writeOrdinaryObject() 方法的含义是把普通的可序列化对象写入流中。普通的含义是除了 String,ObjectStreamClass,ObjectStreamClass,数组,枚举常量之外的并实现了 Serializable 接口的类对象。
private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) throws IOException { try { desc.checkSerialize(); // 写入表示一个新的对象的字节,final static byte TC_OBJECT = (byte)0x73; bout.writeByte(TC_OBJECT); writeClassDesc(desc, false); handles.assign(unshared ? null : obj); if (desc.isExternalizable() && !desc.isProxy()) { writeExternalData((Externalizable) obj); } else { writeSerialData(obj, desc); } } finally { if (extendedDebugInfo) { debugInfoStack.pop(); } } }writeClassDesc(desc, false); 方法表示把类的描述信息 ObjectStreamClass 写入流中,这些信息包括表示类描述的信息,类的全类名信息,实现 Serializable 或 Externalizable 的信息,字段的个数,字段的类型码,字段的名称,非基本类型字段的类型信息。
需要注意的是,writeClassDesc(desc, false) 写入的是类的信息,并不包括对象的信息,即字段的值。
这里不再详述了。
Person2` 没有实现 `Externalizable` 接口,所以 `desc.isExternalizable()` 为 `false`,代码进入 `else` 分支:`writeSerialData(obj, desc);writeSerialData(Object obj, ObjectStreamClass desc) 方法
这个方法的参数值为:
- Object obj, 即 Person2 对象;
- ObjectStreamClass desc,即对应于 Person2 类的类描述信息封装;
writeSerialData() 方法的作用是写入序列化数据,即给定对象的实例化数据,也包括超类的实例化数据。
private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException { ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); for (int i = 0; i < slots.length; i++) { ObjectStreamClass slotDesc = slots[i].desc; if (slotDesc.hasWriteObjectMethod()) { // 返回 false // 省略无关代码 } else { defaultWriteFields(obj, slotDesc); // => 代码走这里 } } }defaultWriteFields(Object obj, ObjectStreamClass desc) 方法
这个方法的参数值为:
- Object obj, 即 Person2 对象;
- ObjectStreamClass desc,即对应于 Person2 类的类描述信息封装;
2.2.2 反序列化流程分析
创建 ObjectInputStream 对象,检查头信息
public ObjectInputStream(InputStream in) throws IOException { verifySubclass(); bin = new BlockDataInputStream(in); handles = new HandleTable(10); vlist = new ValidationList(); serialFilter = ObjectInputFilter.Config.getSerialFilter(); enableOverride = false; readStreamHeader(); bin.setBlockDataMode(true); }readStreamHeader(); 会检查头信息,查看代码:
protected void readStreamHeader() throws IOException, StreamCorruptedException { short s0 = bin.readShort(); short s1 = bin.readShort(); if (s0 != STREAM_MAGIC || s1 != STREAM_VERSION) { throw new StreamCorruptedException( String.format("invalid stream header: %04X%04X", s0, s1)); } }读取头两个 short 值,如果第一个 short 值不等于 STREAM_MAGIC 或第二个short 值不等于STREAM_VERSION,那么就抛出异常:StreamCorruptedException,表示头信息无效。
Object readObject() 方法
public final Object readObject() throws IOException, ClassNotFoundException { return readObject(Object.class); }Object readObject(Class<?> type) 方法
参数的值:
- Class<?> type:Object.class
Object readObject0(Class<?> type, boolean unshared) 方法
参数的值:
- Class<?> type 为 Object.class;
- boolean unshared 为 false
Object readOrdinaryObject(boolean unshared) 方法
参数的值:
- boolean unshared 为 false
readSerialData(Object obj, ObjectStreamClass desc) 方法
这个方法的作用是给实例化的对象字段赋值。
private void readSerialData(Object obj, ObjectStreamClass desc) throws IOException { ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); for (int i = 0; i < slots.length; i++) { ObjectStreamClass slotDesc = slots[i].desc; if (slots[i].hasData) { if (obj == null || handles.lookupException(passHandle) != null) { defaultReadFields(null, slotDesc); // skip field values } else if (slotDesc.hasReadObjectMethod()) { // 省略无关的代码 } else { defaultReadFields(obj, slotDesc); // => 代码走这里 } if (slotDesc.hasWriteObjectData()) { skipCustomData(); } else { bin.setBlockDataMode(false); } } } }defaultReadFields(Object obj, ObjectStreamClass desc) 方法
private void defaultReadFields(Object obj, ObjectStreamClass desc) throws IOException { Class<?> cl = desc.forClass(); if (cl != null && obj != null && !cl.isInstance(obj)) { throw new ClassCastException(); } // 先设置基本类型字段的值 int primDataSize = desc.getPrimDataSize(); if (primVals == null || primVals.length < primDataSize) { primVals = new byte[primDataSize]; } bin.readFully(primVals, 0, primDataSize, false); if (obj != null) { desc.setPrimFieldValues(obj, primVals); } // 再调用 readObject0 设置非基本类型字段的值。 int objHandle = passHandle; ObjectStreamField[] fields = desc.getFields(false); Object[] objVals = new Object[desc.getNumObjFields()]; int numPrimFields = fields.length - objVals.length; for (int i = 0; i < objVals.length; i++) { ObjectStreamField f = fields[numPrimFields + i]; objVals[i] = readObject0(Object.class, f.isUnshared()); if (f.getField() != null) { handles.markDependency(objHandle, passHandle); } } if (obj != null) { desc.setObjFieldValues(obj, objVals); } passHandle = objHandle; }2.3 实际开发中使用 Serializable 接口会遇到的问题
类实现了序列化接口,但是存在没有实现序列化接口的成员,运行报错:java.io.NotSerializableException
需要使用 transient 关键字修饰没有实现序列化接口的成员。值得注意的是,如果没有实现序列化接口的成员变量的值是 null,那么即便不加 transient 关键字也不会报错。
静态变量为什么无法序列化?
静态变量不参与序列化,序列化的是对象的成员字段。
多引用写入问题:同一个引用,多次写入不同的对象内容,但取出的对象是一模一样的
演示代码:
// 序列化 FileOutputStream fos = new FileOutputStream(filePath); ObjectOutputStream oos = new ObjectOutputStream(fos); Person2 personWrite = new Person2("wzc", 32); oos.writeObject(personWrite); personWrite.setAge(33); oos.writeObject(personWrite); oos.close(); // 反序列化 FileInputStream fis = new FileInputStream(filePath); ObjectInputStream ois = new ObjectInputStream(fis); Person2 personRead1 = (Person2) ois.readObject(); Person2 personRead2 = (Person2) ois.readObject(); ois.close(); System.out.println("personWrite:" + personWrite); System.out.println("personRead1:" + personRead1); System.out.println("personRead2:" + personRead2); System.out.println("personRead1 == personRead2:" + (personRead1 == personRead2));打印信息:
personWrite:Person2@692404036{name='wzc', age=33} personRead1:Person2@1072408673{name='wzc', age=32} personRead2:Person2@1072408673{name='wzc', age=32} personRead1 == personRead2:true第一次使用 personWrite 写入的对象内容是 “wzc”, 32;第二次使用 personWrite 写入的对象内容是 “wzc”, 33;但是,反序列化读取到的是一模一样的对象。
解决办法:
- 在第二次写入之前增加代码 oos.reset();
- 把第二次写入的代码:oos.writeObject(personWrite); 替换为 oos.writeUnshared(personWrite);
- 尽量避免多引用写入,使用不同的引用。
父类实现了Serializable,子类没有, 子类是否可以进行序列化?
可以。
子类实现序列化,父类不实现序列化,如何序列化父类的数据?
首先,要给父类添加空参构造器,否则会报错:java.io.InvalidClassException: com.java.advanced.features.io.serialize.serializable.Man; no valid constructor;其次,让子类负责序列化(反序列化)父类的域。代码如下:
public class Person7 { public String name; public int age; // 添加了无参构造器 public Person7() { } public Person7(String name, int age) { this.name = name; this.age = age; } } public class Man3 extends Person7 implements Serializable { public double salary; public Man3(String name, int age, double salary) { super(name, age); this.salary = salary; } private void writeObject(ObjectOutputStream oos) throws IOException { // 先序列化本类对象 oos.defaultWriteObject(); // 再序列化父类的域 oos.writeObject(name); oos.writeInt(age); } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { // 先反序列化本类对象 ois.defaultReadObject();; // 再反序列化父类的域 name = (String) ois.readObject(); age = ois.readInt(); } }序列化的时候多一个字段,反序列化的时候少一个字段,或者序列化的时候少一个字段,反序列化的时候多一个字段,会不会报错?
需要显式地声明 serialVersionUID 的值,如为 1L。
private static final long serialVersionUID = 1L;因为计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。
如果类的 serialVersionUID 是一致的,即便在序列化时的类和反序列化时的类有些不同,该对象仍会被尽最大限度完成反序列化。
writeReplace,writeObject, readResolve,readObject 的执行顺序
writeReplace 先于writeObject, readResolve后于readObject
反序列化打破单例,如何解决?
给单例添加 readResovle() 方法:
public class SingletonSerializeFix implements Serializable { private static final long serialVersionUID = 1L; private SingletonSerializeFix() { //no instance } public static SingletonSerializeFix getInstance() { return SingletonHolder.instance; } private static class SingletonHolder { private static SingletonSerializeFix instance = new SingletonSerializeFix(); } private Object readResolve() { return SingletonHolder.instance; } }