Java对象序列化剖析

对象序列化的目的

1)希望将Java对象持久化在文件中

2)将Java对象用于网络传输


实现方式

如果希望一个类的对象可以被序列化/反序列化,那该类必须实现java.io.Serializable接口或java.io.Externalizable接口,前者为一个标记接口,即不存在任何需要实现的方法,仅仅为一种对象可序列化的标识,且序列化和反序列化的方式为系统默认方式;而后者其实内部也实现了Serializable,并且包含两个方法writeExternal()和readExternal()分别用于对象的写入与读取,因此可以采用自定义的方式进行序列化/反序列化,比Serializable更灵活。

而在编写序列化类时还有一个重要的属性serialVersionUID,该属性为long型,且必须为static和final,用于校验序列化时的对象与反序列化出的对象是否为同一个类对应的对象。该值的生成不建议使用系统动态提供的默认值,而应使用IDE自带的UID生成功能。


代码示例

写一个简单的类Person,实现了Serializable接口。注意到该类有四个属性,其中company为static,age为transient。

public class Person implements Serializable {

    private static final long serialVersionUID = 7686111088673836789L;

    private int id;

    private String name;

    private static String company;

    private transient int age;

    static {
company = "Apple";
} public Person(int id, String name, int age) {
super();
this.id = id;
this.name = name;
this.age = age;
} @Override
public String toString() {
return "Person [id=" + id + ", name=" + name + ", age=" + age + "]";
} }

一旦一个类可序列化之后,我们就可以通过ObjectOutputStream / ObjectInputStream将该类的某个对象序列化存储在文件中,并随后从文件中反序列出来还原该对象。

public class SerializableTest {

    /** 将对象序列化至文件 */
public void serialize(Serializable object, String filePath)
throws IOException {
File file = null;
ObjectOutputStream out = null; try {
file = new File(filePath);
out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(object);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null)
out.close();
}
} /** 将对象从文件中反序列化出来 */
public Object deserialize(String filePath) throws IOException {
File file = null;
ObjectInputStream in = null; try {
file = new File(filePath);
in = new ObjectInputStream(new FileInputStream(file));
return in.readObject();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (in != null)
in.close();
}
return null;
} public static void main(String[] args) throws IOException {
SerializableTest test = new SerializableTest(); String filePath = "output/person.ser";
test.serialize(new Person(24, "kobe", 37), filePath);
Person person = (Person) test.deserialize(filePath);
System.out.println("person = " + person);
}
}

在此,我们生成了一个Person对象,并传入了三个参数,随后将其存储到person.ser文件中,并接着从该文件中读取反序列化出该对象。最后的输出为:

person = Person [id=24, name=kobe, age=0]

id和name两个值没有问题,可为何age=0,而不是传入的37呢?这就是transient这个关键字的作用了,原来并不是所有的属性都会参与序列化过程,比如用static和transient关键字标识的属性。因为static属性意味着是该类的全部对象所共同拥有的,并不属于某个具体的对象,因此不应该在对象序列化的时候出现;而transient关键词的意义则是为了在某些特定属性(如某些敏感的密码信息等)不希望其存储在文件中时人为添加上去的。当然,其实有另一种反向确定可序列化属性的方法,即定义一个类型为ObjectStreamField[]、名称为serialPersistentFields的特殊属性,并且它必须是private、static和final的,由serialPersistentFields就可以确定哪些属性是希望参与序列化的,甚至包括默认不可序列化的static和transient属性(static属性本人并未测试成功,总是抛出"unmatched serializable field(s) declared"异常信息,请各位大牛不惜赐教)。例如:

public class Person implements Serializable {

    private static final long serialVersionUID = -4481514608698081671L;

    private transient Integer id;

    private String name;

    private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField("id", Integer.class) };
}

通过这种方法,具有transient特性的id会被序列化,而原本默认会被序列化的name却不会被序列化,因为serialPersistentFields中仅指定了id为可序列化属性。


序列化流协议存储格式

将之前那个Person对象序列化至文件后,可用Binary Viewer二进制查看工具(Windows平台下)打开,让我们一起了解一下序列化文件的存储格式,内容如下图所示(注意只有id和name属性可序列化,company和age并不会)。

Java对象序列化剖析

这些十六进制的数字代表的含义遵循Java对象序列化流协议,下面依次介绍它们的含义。

AC ED:STREAM_MAGIC
00 05:STREAM_VERSION
73:TC_OBJECT
72:TC_CLASSDESC
00 06:类名的长度,即Person的长度,为6
50 65 72 73 6F 6E:类名,即Person
6A AA 10 8D 4F F8 98 4E F5:serialVersionUID的值,为7686111088673836789
02:classDescFlags,在此为SC_SERIALIZABLE,因为该类实现了Serializable接口
00 02:属性个数,在此可序列化的属性个数为2
49:I,表示int,因为id是int类型
00 02:属性名称长度,即id的长度,为2
69 64:属性名称,即id
4C:L,表示类类型,因为String是类类型,区别于基本类型
00 04:属性名称的长度,即name的长度,为4
6E 61 6D 65:属性名称,即name
74:TC_STRING,表示字符串
00 12:类型名称长度,即下面的"Ljava/lang/String;"的长度,为18
4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B:Ljava/lang/String;其中L表示类类型(注意结尾有个;分号)
78:TC_ENDBLOCKDATA
70:TC_NULL
00 00 00 18:属性值,即id的值,为24
74:TC_STRING,表示字符串
00 04:属性值的长度,即name的值kobe的长度,为4
6B 6F 62 65:属性值,即name的值,为kobe


自定义序列化/反序列化过程

凡是实现了Serializable接口的类其实都有几个隐藏的方法可重写,进而控制整个序列化与反序列化的过程,如:

private void writeObject(ObjectOutputStream out) throws IOException;

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

Object writeReplace() throws ObjectStreamException;

Object readResolve() throws ObjectStreamException;

其中writeReplace()和readResolve()方法为钩子函数,分别在writeObject()调用之前和readObject()调用之后被调用。其中readResolve()方法在本人另一篇关于Singleton的博文中出现过,目的就是防止不怀好意之人通过对象序列化技术人为破坏单例模式,构造出新的对象,因为通过readObject()方法可以直接构造新对象而避开私有构造方法。但正因为readResolve()方法在readObject()反序列化对象后被调用,因此可用来替换读取出的流对象。


References

[1] https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html#a8299

[2] https://docs.oracle.com/javase/8/docs/platform/serialization/spec/serial-arch.html#a6250

[3] http://www.javaworld.com/article/2072752/the-java-serialization-algorithm-revealed.html

[4] https://www.javacodegeeks.com/2015/09/built-in-serialization-techniques.html

[5] http://steven2011.iteye.com/blog/1299499

[6] http://*.com/questions/1168348/java-serialization-readobject-vs-readresolve


为尊重原创成果,如需转载烦请注明本文出处:http://www.cnblogs.com/fernandolee24/p/5682738.html,特此感谢

上一篇:iOS设计模式之原型模式


下一篇:Java对象序列化---转载