序列化是一种Java对象持久化的手段,普遍应用在网络传输、RMI等场景中,这篇文章将深入讲解 Java 的序列化的原理以及如何自定义序列化策略。 |
Java 对象的序列化原理
☕️ 为什么要序列化
一般情况下,我们在 Java 平台创建的对象只有在 JVM 运行时才会存在,即这些对象的生命周期不会比 JVM 的生命周期更长。但在现实应用中,可能需要在 JVM 停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。所以,为了实现这样的需求就有了我们的序列化功能。
使用 Java 对象序列化保存对象时,会把对象的状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是:对象序列化保存的是对象的”状态”,即它的成员变量,而不会保存类中的静态变量。
除了在持久化对象时会用到对象序列化之外,当使用 RMI(Romote Method Invocation,远程方法调用),或在网络中传递对象时,都会用到对象序列化,并且 Java 序列化 API 为处理对象序列化提供了一个标准机制,该API简单易用。
☕️ Java对象的序列化和反序列方式
在Java中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化。我们以一个 demo 代码为例:
创建一个用于序列化的 Person 类
public class Person implements Serializable {
private String name;
private transient int age;
private static final long serialVersionUID = -6849794470754667710L;
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return this.age;
}
@Override
public String toString() {
return "Person { " +
"name = '" + this.name + "', " +
"age = " + this.age + " }";
}
}
对 Person 进行序列化和反序列化
public static void main(String[] args) {
// 创建一个待序列化的对象
Person person = new Person();
person.setName("Andy");
person.setAge(24);
System.out.println(person);
// 把对象序列化到文件中去
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person"));
oos.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}
// 从文件中反序列化成对象
File file = new File("person");
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person"));
Person newPerson = (Person) ois.readObject();
System.out.println(newPerson);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
>>>>>
Person { name = 'Andy', age = 24 }
Person { name = 'Andy', age = 0 }
现在我们来介绍一下对象序列化和反序列化的相关重要知识:
- 一个类只有实现了 java.io.Serialzable 接口才可以被序列化。
- 通过 ObjectOutputStream 和 ObjectInputStream 来进行对象的序列化和反序列化。
- 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,而且还取决于两个类的序列化ID(private static final long serialVersionUID)是否一致。
- 序列化不保存静态变量,因为序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。
- 要想将父类对象也序列化,就需要让父类也实现 Serializable 接口。
- Transient 关键字的作用是控制变量的序列化:阻止该变量被序列化到文件中。在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
提示:服务器端给客户端发送序列化对象数据时,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
☕️ 序列化存储规则
Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用和一些控制信息的空间,反序列化时,恢复引用关系,所以写入同一个对象多次后反序列化得到的对象是指向同一个对象的引用。该存储规则极大的节省了存储空间。
比如下面的代码例子:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("result.obj"));
Test test = new Test();
test.num = 1;
oos.writeObject(test);
oos.flush();
test.num = 2;
oos.writeObject(test);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("result.obj"));
Test t1 = (Test) ois.readObject();
Test t2 = (Test) ois.readObject();
System.out.println("t1.num = " + t1.num);
System.out.println("t2.num = " + t2.num);
>>>>>
t1.num = 1
t2.num = 1
本案例的目的是希望将 test 对象两次保存到 result.obj 文件中,写入一次以后修改对象属性值再次保存第二次,然后从 result.obj 中再依次读出两个对象,输出这两个对象的 num 属性值。案例代码的目的原本是希望一次性传输对象修改前后的状态。
结果两个输出的都是 1, 原因就是第一次写入对象以后,第二次再试图写的时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象。因此在使用同一个文件多次 writeObject 时,需要特别注意这个问题。
自定义序列化和反序列化方式
我们通过 ArrayList 来了解一下如何进行自定义序列化和反序列化。我们先写一个关于 ArrayList 序列化和反序列化的例子吧:
public static void main(String[] args) {
// 创建待序列化ArrayList对象
List<String> strList = new ArrayList<>();
strList.add("Alpha");
strList.add("Beta");
strList.add("Gamma");
System.out.println("origin list: " + strList);
// 序列化ArrayList对象
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("stringList"));
oos.writeObject(strList);
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化得到新的ArrayList对象
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("stringList"));
List<String> newStrList = (ArrayList<String>)ois.readObject();
System.out.println("new list: " + newStrList);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
>>>>>
origin list: [Alpha, Beta, Gamma]
new list: [Alpha, Beta, Gamma]
代码的结果和我们预期的一致,好,那么我们去看一下 ArrayList 的源码:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
transient Object[] elementData; // non-private to simplify nested class access
private int size;
}
从源码我们可以看出,ArrayList 确实是实现了 Serializable 接口,这就说明可以对它进行序列化及反序列化操作。但存放数据的 elementData 数组是 transient 的,那么在序列化过程中,本应该数据不会被保存下来才对的,但为什么保留下来了呢?答案就是:ArrayList 定义了来个方法: writeObject 和 readObject
在介绍 writeObject 和 readObject 之前我们需要先了解一下重要的知识点:
- 在序列化过程中,如果被序列化的类中定义了 writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化
* 如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法 - 用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值
好了,现在我们进去看一下这两个方法里面的具体实现源码:
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioral compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
那么为什么 ArrayList 要用这种方式来实现序列化呢?
我们知道,ArrayList 实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为 100,而实际只放了一个元素,那就会序列化 99 个 null 元素。所以,为了优化存储以及在序列化的时候不会将这么多 null 同时进行序列化,ArrayList 把元素数组设置为了 transient,并通过重写 writeObject 和 readObject 方法的方式把其中的元素保留下来
现在我们来分析一下,重写的 writeObject 和 readObject 是如何被调用的:
对象的序列化过程通过 ObjectOutputStream 和 ObjectInputputStream 来实现的,我们以 ObjectOutputStream 为例,ObjectOutputStream 序列化时使用的是 writeObject 方法,那好,我们就进去源码看一下,限于篇幅,这里我一步步跟踪得到 writeObject 方法的调用栈为:
writeObject >> writeObjecto >> writeOrdinaryObject >> writeSerialData >> invokeWriteObject |
我们来看一下 invokeWriteObject 的源码:
void invokeWriteObject(Object obj, ObjectOutputStream out)
throws IOException, UnsupportedOperationException{
if (writeObjectMethod != null) {
try {
writeObjectMethod.invoke(obj, new Object[]{ out });
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof IOException) {
throw (IOException) th;
} else {
throwMiscException(th);
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
其中 writeObjectMethod.invoke(obj, new Object[]{ out }) 是关键,通过反射的方式调用 writeObjectMethod 方法,官方是这么解释这个 writeObjectMethod 的:
class-defined writeObject method, or null if none |
所以,一个类的 writeObject 和 readObject 方法,在使用 ObjectOutputStream 的 writeObject 方法和 ObjectInputStream 的 readObject 方法时,会通过反射的方式调用。
📖 总结:
- 如果一个类想被序列化,需要实现Serializable接口。否则将抛出NotSerializableException异常,这是因为,在序列化操作过程中会对类型进行检查,要求被序列化的类必须属于Enum、Array和Serializable类型其中的任何一种
- 在变量声明前加上 transient 关键字,可以阻止该变量被序列化到文件中
- 在类中增加writeObject 和 readObject 方法可以实现自定义序列化方式