在程序中为了能直接以 Java 对象的形式进行保存,而后再从新获得该 Java 对象,这就须要序列化能力。序列化其实能够当作是一种机制,按照必定的格式将 Java 对象的某状态转成介质可接受的形式,以方便存储或传输。其实想一想就大体清楚基本流程,序列化时将 Java 对象相关的类信息、属性及属性值等等保存起来,反序列化时再根据这些信息构建出 Java 对象。而过程可能涉及到其余对象的引用,因此这里引用的对象的相关信息也要参与序列化。java
Java 中进行序列化操做须要实现 Serializable 或 Externalizable 接口。算法
FileOutputStream f = new FileOutputStream("tmp.o");
ObjectOutput s = new ObjectOutputStream(f);
s.writeObject("test");
s.writeObject(new ArrayList());
s.flush();
复制代码
常见的使用方式是直接将对象写入流中,好比上述例子中,建立了 FileOutputStream 对象,其对应输出到 tmp.o 文件中,而后建立 ObjectOutputStream 对象嵌套前面的输出流。当咱们调用 writeObject 方法时即能进行序列化操做。数组
writeObject 方法这里须要说明下,在对某个对象进行写入时,它其实不只仅序列化本身,还会去遍历寻找相关引用的其余对象,由本身和其余引用对象组成的一个完整的对象图关系都会被序列化。bash
对于数组、enum、Class类对象、ObjectStreamClass 和 String 等都会作特殊处理,而其余对象序列化则须要实现 Serializable 或 Externalizable 接口。并发
FileInputStream in = new FileInputStream("tmp.o");
ObjectInputStream s = new ObjectInputStream(in);
String test = (String)s.readObject();
List list = (ArrayList)s.readObject();
复制代码
针对序列化则存在反序列化操做,经过流直接读取对象,先建立 FileInputStream 对象,其对应输入文件为 tmp.o,而后建立 ObjectInputStream 对象嵌套前面的输入流,接着则能够调用 readObject 方法读取对象。机器学习
其中调用 readObject 方法反序列操做的过程,除了会恢复对象本身以外还会遍历整个完整的对象图,建立整个对象图包含的全部对象。分布式
在序列化操做时,常常会看到实现了 Serializable 接口的类会存在一个 serialVersionUID 属性,而且它是一个固定数值的静态变量。好比以下,这个属性有什么做用?其实它主要用于验证版本一致性,每一个类都拥有这么一个 ID,在序列化的时候会一块儿被写入流中,那么在反序列化的时候就被拿出来跟当前类的 serialVersionUID 值进行比较,二者相同则说明版本一致,能够序列化成功,而若是不一样则序列化失败。函数
private static final long serialVersionUID = -6849794470754667710L;
复制代码
通常状况下咱们能够本身定义 serialVersionUID 的值或者 IDE 帮咱们自动生成,而若是咱们不显示定义 serialVersionUID 的话,这不表明不存在 serialVersionUID,而是由 JDK 帮咱们生成,生成规则是会利用类名、类修饰符、接口名、字段、静态初始化信息、构造函数信息、方法名、方法修饰符、方法签名等组成的信息,通过 SHA 算法生成摘要便是最终的 serialVersionUID 值。学习
若是一个子类实现了 Serializable 接口而父类没有实现该接口,则在序列化子类时,子类的属性状态会被写入而父类的属性状态将不被写入。因此若是想要父类属性状态也一块儿参与序列化,就要让它也实现 Serializable 接口。ui
另外,若是父类未实现 Serializable 接口则反序列化生成的对象会再次调用父类的构造函数,以此完成对父类的初始化。因此父类属性初始值通常都是类型的默认值。好比下面,Father 类的属性不会参与序列化,反序列化时 Father 对象的属性的值为默认值0。
public class Father {
public int f;
public Father() {
}
}
public class Son extends Father implements Serializable {
public int s;
public Son() {
super();
}
}
复制代码
在序列化时类的哪些字段会参与到序列化中呢?其实有两种方式决定哪些字段会被序列化,
能够看到普通的字段都是默认会被序列化的,而对于某些包含敏感信息的字段咱们不但愿它参与序列化,那么最简单的方式就是能够将该字段声明为 transient。
如何使用 ObjectStreamField?举个例子,以下,A类中有 name 和 password 两个字段,经过 ObjectStreamField 数组声明只序列化 name 字段。这种声明的方式不用纠结为何这样,这仅仅是约定了这样而已。
public class A implements Serializable {
String name;
String password
private static final ObjectStreamField[] serialPersistentFields
= {new ObjectStreamField("name", String.class)};
}
复制代码
Enum 类型的序列化与普通的 Java 类的序列化有所不一样,那么在深刻以前能够先看这篇文章深刻了解下枚举,《 从JDK角度认识枚举enum》。
因此咱们知道枚举被编译后会变成一个继承 java.lang.Enum 的类,并且枚举里面的元素被声明成 static final ,另外生成一个静态代码块 static{},最后还会生成 values 和 valueOf 两个方法。Enum 类是一个抽象类,主要有 name 和 ordinal 两个属性,分别用于表示枚举元素的名称和枚举元素的位置索引。
Enum 类型参与序列化时只会将枚举对象中的 name 属性写入,而其余的属性则不参与进来。在反序列化时,则是先读取 name 属性,而后再经过 java.lang.Enum 类的 valueOf 方法找到对应的枚举类型。
除此以外,不能自定义 Enum 类型的序列化,因此 writeObject, readObject, readObjectNoData, writeReplace 以及 readResolve 等方法在序列化时会被忽略,相似的,serialPersistentFields 和 serialVersionUID 属性都会被忽略。
最后,在序列化场景中,涉及到使用枚举的状况时要仔细设计好,否则极可能会由于后面升级修改了枚举类的结构而致使反序列化失败。
Externalizable 接口主要就是提供给用户本身控制序列化内容,虽然前面咱们也看到了 transient 和 ObjectStreamField 能定义序列化的字段,但经过 Externalizable 接口则能更加灵活。能够看到它其实继承了 Serializable 接口,提供了 writeExternal 和 readExternal 两个方法,也就是在这两个方法内控制序列化和反序列化的内容。
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
复制代码
好比下面的例子,咱们能够在 writeExternal 方法中额外写入 Date 对象,而后再写入 value 值。对应的,反序列化时则是在 readExternal 方法中读取 Date 对象和 value。这样就完成了自定义序列化操做。
public class ExternalizableTest implements Externalizable {
public String value = "test";
public ExternalizableTest() {
}
public void writeExternal(ObjectOutput out) throws IOException {
Date d = new Date();
out.writeObject(d);
out.writeObject(value);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
Date d = (Date) in.readObject();
System.out.println(d);
System.out.println((String) in.readObject());
}
}
复制代码
正常状况下序列化某个对象时写入的正是当前的对象,但若是说咱们要替换当前的对象而写入其余对象的话则能够经过 writeReplace 方法来实现。好比下面,person 类经过 writeReplace 方法最终能够写入 Object 数组对象。因此咱们在反序列化时就再也不是转换成 Person 类型,而是要转换为 Object 数组对象。
class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private Object writeReplace() throws ObjectStreamException {
Object[] properties = new Object[2];
properties[0] = name;
properties[1] = age;
return properties;
}
}
复制代码
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.o"));
Object[] properties = (Object[]) ois.readObject();
复制代码
上面介绍了在写入时能够替换对象,而在读取时也一样支持替换对象的,它是经过 readResolve 方法实现的。好比下面,在 readResolve 方法返回 2222,则反序列化读取时再也不是 Person 对象,而是 2222。
class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private Object readResolve() throws ObjectStreamException {
return 2222;
}
}
复制代码
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.o"));
Object o = ois.readObject();
复制代码
-------------推荐阅读------------
------------------广告时间----------------
公众号的菜单已分为“分布式”、“机器学习”、“深度学习”、“NLP”、“Java深度”、“Java并发核心”、“JDK源码”、“Tomcat内核”等,可能有一款适合你的胃口。
鄙人的新书《Tomcat内核设计剖析》已经在京东销售了,有须要的朋友能够购买。感谢各位朋友。
欢迎关注: