关于 Java 中的序列化,最核心的两个接口为 Serializable
与 Externalizable
。
Serializable
的源码如下:
1 | public interface Serializable { |
Externalizable
的源码如下:
1 | public interface Externalizable extends java.io.Serializable { |
可以看出 Externalizable
继承了 Serializable
接口且额外定义了两个方法。关于它们之前的区别可以参见 What is the difference between Serializable and Externalizable in Java?。具体实现可以跟随 JDK 源码 ObjectOutputStream.writeObject 中的调用链至 ObjectOutputStream.writeOrdinaryObject 可以看出对两个接口的处理差异,此处不再一一分析。
在序列化中,不得不提的就是 transient
关键字,对于该关键字,在 JLS 中只有以下简短的描述:
Variables may be marked transient to indicate that they are not part of the persistent state of an object.
在常见的集合框架类的相关源码中,经常看到 transient
的身影,作者为何要加上此关键字呢?
比如在 ArrayList
类的源代码中,可以看到:
1 | /** |
详细的解释可以参见 Why does ArrayList use transient storage?,参考 ArrayList
类中的 writeObject(java.io.ObjectOutputStream s)
及 readObject(java.io.ObjectInputStream s)
方法的源码:
1 | /** |
可以看到,作者仅将 ArrayList
中实际存储的 size 个元素进行了序列化操作,从而避免了 Object[] elementData
中未使用的部分进行序列化,这样的实现性能更优。
同样的,在 HashMap
的源码中,我们可以看到如下声明:
1 | /** |
如果看了 ArrayList
的源码我们可以知道仅仅序列化 size 个元素相比序列化整个数组性能更优,在 HashMap
中除了这个原因还有其他的原因吗?可以参见 Confusion of HashMap Keyword transient in Java 与 Explanation of transient keyword in HashMap,主要原因是需要考虑到 key 的 hashCode
方法没有被覆写的情况,此时会采用 Object 的默认 hashCode
方法实现,其中源码为:
1 | /** |
这个方法为 native
方法,其中 Java Doc 有提到:
This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the Java™ programming language
可以看出,hashCode
方法的典型实现为转换对象的内部地址为整数,但是 Java 编程语言没有要求一定这样实现。我们再看看 OpenJDK 8 synchronizer.cpp 中 hashCode
的 c 语言实现:
1 | // hashCode() generation : |
通过在 JDK 8 的版本上执行 java -XX:+PrintFlagsFinal -version | grep hashCode
可以看到输出如下:
1 | intx hashCode = 5 {product} |
可以看出在 JDK 8 使用的是 Marsaglia XOR-Shift 算法实现的 native hashCode
方法,并没有采用基于内存地址的实现,如果你在本地执行如下代码:
1 | package me.tianshuang; |
你会发现,多次执行会得到相同的结果,但是更换一个 JVM 进行执行,会发现结果发生了变化。
即使在实现机制相同的情况下,更换了 JVM 后 native hashCode
的返回值依然会变化,那么如果我们将 HashMap
的 Node<K,V>[] table
进行了序列化,且此 HashMap 存储的 key 没有覆写 hashCode
方法,此时在另一个 JVM 进行反序列化后,因为 hashCode
的返回值发生了变化,计算出的 tabIndex
就可能与之前的 tabIndex
不同,此时会导致获取不到元素。所以 HashMap
的实现是将所有元素依次进行序列化然后反序列化的时候重建的 HashMap
映射关系,以保证经过序列化及反序列化后当 hashCode
值发生改变后不影响 HashMap
的功能。
关于这一点,其实在 《Effective Java中文版(第3版)》 中也有提到,如第 12 章第 87 条:考虑使用自定义的序列化形式:
For example, consider the case of a hash table. The physical representation is a sequence of hash buckets containing key-value entries. The bucket that an entry resides in is a function of the hash code of its key, which is not, in general, guaranteed to be the same from implementation to implementation. In fact, it isn’t even guaranteed to be the same from run to run. Therefore, accepting the default serialized form for a hash table would constitute a serious bug. Serializing and deserializing the hash table could yield an object whose invariants were seriously corrupt.
最后附上 HashMap
的序列化/反序列化实现代码:
1 | /** |
Reference
Serializable (Java Platform SE 8 )
Externalizable (Java Platform SE 8 )
Spring Data Redis
Java Object Serialization Specification: 5 - Versioning ofSerializable Objects