前言

序列化是一种对象持久化的手段,使用Java对象序列化,保存对象时会将其状态保存为一组字节,在之后可以再将这些字节组装成对象。简单来说,要实现Java对象的序列化,我们只需要让被序列化类实现Serializable接口,并且使用ObjectInputStreamObjectOutputStream进行对象的读写即可。但是,关于Java序列化和反序列化其实还有一些更深层次的特性需要了解。

如何实现对象的序列化

首先,先介绍一下如何将对象序列化并反序列化。在Java中,被序列化的类必须实现Serializable接口,然后通过ObjectInputStreamObjectOutputStream进行对象的读写即可实现对象的序列化与反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Person implements Serializable {
String name;
Integer age;

public Person(String name, Integer age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person("LiHua", 19);

ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream("test.txt"));
oo.writeObject(person);
oo.close();

ObjectInputStream oi = new ObjectInputStream(new FileInputStream("test.txt"));
Person anotherPerson = (Person) oi.readObject();
oi.close();

System.out.println(person);
System.out.println(anotherPerson);
System.out.println(person == anotherPerson);
}
}

控制台输出如下:

1
2
3
Person{name='LiHua', age=19}
Person{name='LiHua', age=19}
false

可以看出,对象在序列化到文件后,可以再通过反序列化重新加载进内存,不过虽然这两个对象的属性值都相同,可是它们并不是同一个对象,它们的内存地址并不相同。

这里需要注意的是,被序列化类虽然实现了Serializable接口,但这个接口并不包含任何方法,仅仅起到标识的作用,那么它是在什么地方起到作用的呢?这里就要从ObjectOutputStreamwriteObject方法的调用栈去寻找:writeObject->writeObject0->writeOrdinaryObject->writeSerialData->invokeWriteObject,在writeObject0这个方法中有这么一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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());
}
}

也就是说,在序列化时该方法会先判断被序列化的类是否是StringArrayEnumSerializable类型,如果不是则直接抛出NotSerializableException异常。

serialVersionUID

虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,还有一个非常重要的一点是两个类的序列化ID是否一致,也就是我们可以在被序列化的类中加上private static final long serialVersionUID = 1L;来控制该类的序列化版本号,如果序列化与反序列化的该属性不同,则会抛出异常。这个序列化ID可以是随机的一个不重复的long型数值,但是如果没有特殊需求的话,使用默认的1L就可以了。

transient关键字

transient关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient变量的值被设为初始值,如int型的是0,对象型的是null。但是,其实我们也可以使用自定义的序列化和反序列化策略,将transient修饰过的变量序列化到文件中,在ArrayList中就有这样的应用:

1
transient Object[] elementData;

我们知道,ArrayList的本质其实就是通过数组存储元素,但是查看源码会发现这个存储元素的数组elementDatatransient修饰了,这并不意味着它就无法被序列化了,相反,ArrayList通过writeObjectreadObject方法以自定义的方式将其序列化并反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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 behavioural 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
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, 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();
}
}
}

在序列化的过程中,如果被序列化的类定义了writeObjectreadObject方法,虚拟机会试图调用这两个方法进行用户自定义的序列化和反序列化,如果没有这两个方法,则默认调用是ObjectOutputStreamdefaultWriteObject方法以及ObjectInputStreamdefaultReadObject方法。

因此,如果我们想在序列化的过程中动态改变序列化的数值,就可以定义这两个方法。典型的应用场景就是在序列化对象数据时,有一些数据是敏感的,我们想在序列化时进行加密,而在反序列化时进行解密,那么此时就可以通过这两个方法来实现。

那么在上面的ArrayList中,又为什么要用这种方式实现序列化呢?原因是因为ArrayList实际上是动态数组,每次放满元素后都会自动扩容,此时如果实际的元素个数小于容量大小时,会将null元素也序列化到文件中。为了保证只序列化实际存在的元素,ArrayListelementDatatransient关键字修饰,并自定义了序列化反序列化的策略。

那么这个自定义的序列化反序列化策略又是在哪调用的?还是上面的调用栈writeObject->writeObject0->writeOrdinaryObject->writeSerialData->invokeWriteObject,在writeSerialData方法中我们可以看到这样一段逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (slotDesc.hasWriteObjectMethod()) {
// ...
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bout.setBlockDataMode(true);
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
} finally {
// ...
}

curPut = oldPut;
} else {
defaultWriteFields(obj, slotDesc);
}

它会先通过hasWriteObjectMethod判断存在用户自定义的writeObject方法后,调用invokeWriteObject方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void invokeWriteObject(Object obj, ObjectOutputStream out)
throws IOException, UnsupportedOperationException
{
requireInitialized();
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 });是关键,正是在这里通过反射的方式调用自定义的writeObject方法的。

父类与静态变量序列化

关于序列化反序列化机制还有几点需要注意的是,静态变量是无法被序列化的,原因在于序列化保存的是对象的状态,而静态变量属于类的状态。除此之外,如果被序列化的类的父类没有实现Serializable接口时,虚拟机是不会序列化父对象的,要想将父类对象也序列化,就需要让父类也实现Serializable接口。因此,如果我们想要让某些字段不被序列化,可以将这些字段抽取出来放到父类中,且让子类实现Serialzable接口,父类不实现。

总结

  • 在Java中,只要一个类实现了Serializable接口,那么就可以通过ObjectInputStreamObjectOutputStream将其对象进行序列化与反序列化。
  • 如果两个类的serialVersionUID不同,则无法被反序列化,此时会抛出异常。
  • 序列化不保存静态变量。
  • 要想将父类对象也序列化,就需要让父类也实现Serializable接口。
  • 将被序列化的类的字段用transient关键字修饰,可以阻止该字段被序列化,在被反序列化时,该变量的值会被设为初始值。
  • 可以通过定义writeObjectreadObject方法实现自定义的序列化反序列化策略,如对敏感数据进行加密与解密。

参考资料