注意:本文语言环境为:max os x 10 ,jdk 1.8
前言
对于一个对象实例,如果我想在系统重启后,仍能重现。那么这个时候通过数据库持久化可以将对象实例存储到数据库内,在需要的时候再取出来,进行对象的复现。但是如果我想在进行远程调用或者跨平台进行数据传输时,对方接收到的对象实例和我传输的是一致的,那么这个时候如果实现呢?请看下面这段代码:
public class Logon {
Date date = new Date();
String username;
public Logon(String name, String pwd) {
username = name;
password = pwd;
}
}
如果在进行数据传输时,不能保证对象的实例是同一个,对于Logon类而言,那么date属性的值就不能保证是一致的。
如果我想保持对象实例的一致性,那么怎么做呢?实现序列化接口即可。
public class Logon implements Serializable {
Date date = new Date();
String username;
public Logon(String name, String pwd) {
username = name;
password = pwd;
}
}
1、标记接口
看到这,我们就有一个疑问:为嘛序列化接口是一个空接口,凭声明一个空接口就可以进行对象实例的持久化?
我们来看jdk源码内的序列化接口声明:
public interface Serializable {
}
这种接口也称为标记接口(类似的还有cloneable),只需要类实现该接口即可,jvm底层自动会帮你完成这个接口的特殊功能(在有的情况下)。
2、序列化接口实现原理
但是到这,我们上面的疑问其实还没解答啊。
序列化接口作为一种将对象转换为流数据的方法,必须通过ObjectOutputStream/ObjectInputStream对象进行流的传输。在进行流传输时,ObjectOutputStream/ObjectInputStream会调用其对应的writeObject/readObject方法进行对象的序列化操作(具体的代码我就不贴了,太长了)。
这个时候就有一个问题, 我就不想用系统自带的序列化方法,我想自己自定义,怎么办?
好办,在类内添加如下方法。
private void writeObject(java.io.ObjectOutputStream s)
throws IOException;
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException;
写完这个代码之后,心里虚啊,这里有几个奇怪的地方。
1)既然是实现接口的功能,为嘛这个访问权限是private
2)方法写完了之后,在这个类内都没有调用的地方。
这2个问题,其实我也不知道怎么解释(坐等高人……)。其实,在看源码之后(代码在手,天下我有),可以给出如下的解释(还是debug靠谱):
首先,在这个类内没有地方调用,是因为这2个方法是在ObjectOutputStream/ObjectInputStream内调用的,是由这2个类内的writeObject/readObject调用对象内覆写的writeObject/readObject方法进行对象的序列化操作。
至于private访问权限,有万能的反射。
来,我们看ObjectOutputStream的代码:
private void writeSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
//判断是否覆写了writeObject方法
if (slotDesc.hasWriteObjectMethod()) {
PutFieldImpl oldPut = curPut;
curPut = null;
SerialCallbackContext oldContext = curContext;
if (extendedDebugInfo) {
debugInfoStack.push(
"custom writeObject data (class \"" +
slotDesc.getName() + "\")");
}
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bout.setBlockDataMode(true);
//反射调用覆写的writeObject方法
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
} finally {
curContext.setUsed();
curContext = oldContext;
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
curPut = oldPut;
} else {
//调用默认的writeObject方法
defaultWriteFields(obj, slotDesc);
}
}
}
3、自定义序列化的另一种方式
上面不是提到了自定义序列化方式吗,除了覆写2个方法外,还有一种方式,就是实现Externalizable接口。
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
4、transient关键字
上面介绍的都是自定义序列化方法,但是对于一个成员变量,如果我不想对其进行序列化操作,这个时候如果仅采用自定义序列化,那么肯定要完全重写ObjectOutputStream/ObjectInputStream内的序列化/反序列化代码,否则一旦调用defaultWriteObject/defaultReadObject,那么成员变量就自动进行了序列化的操作。
此时transient关键字就发挥作用了。这个关键字会告诉defaultWriteObject方法,这个成员变量不用进行序列化了。如果一个成员变量被transient修饰符修饰了,但是又想进行序列化怎么办?就重写该类的writeObject /readObject方法吧,在这2个方法内自定义序列化与反序列化。
5、序列化与反序列化
对于上面的Logon类,生成一个对象实例,并序列化后,再新增一个属性password(可以将序列化完成的结果持久化到硬盘),变成如下类:
public class Logon implements Serializable {
private Date date = new Date();
private String username;
private String password;
}
那么再进行反序列化调用readObject时,会发生什么呢?
会抛出ClassNotFoundException。为什么会这样呢?因为序列化实际上会自动生成一个版本信息,类似于这样:
public class Logon implements Serializable {
private static final long serialVersionUID = -5664935080424674771L;
private Date date = new Date();
private String username;
private String password;
}
那如果没生成serialVersionUID属性会怎样?那么此时序列化机制会根据对象内的成员属性自动生成一个序列化版本号(猜测类似于hashcode),当对象的成员变量有变更时,那么serialVersionUID就会不一致,反序列化时就会认为这2个不是一个类,抛ClassNotFoundException也在情理之中了。
如何避免这种情况呢?正常来说,最好在实现序列化接口时,手动生成一个版本信息。那么即使对象的成员变量有变更,在进行反序列化时也会将未变更的成员变量进行自动反序列化。
还有一种情况,就是父类实现了序列化接口,但是子类没有。那么如果父类有变更,子类的序列化版本会发生变化吗?不会!(原因,我也不知道,这么乱,我也是醉了)
如果系统是使用maven进行版本管理,如果成员变更,但是序列化版本没变,那么此时可以考虑进行版本升级,避免不同版本之间进行错误的序列化操作。
6、其他情况
对于static修饰的成员变量,也是不会进行序列化的。因为所谓的序列化,是对对象的实例而言,而一个类的static成员变量,是一个类共享的,不属于任何一个对象实例,因而不会将static成员变量进行序列化。如果想序列化static成员变量,那么就需要自定义序列化与反序列化。
7、实例
上面讲了这么多,我们来段代码分析分析,加深一下印象。还是对hashmap源码进行分析(为嘛我就跟hashmap过不去呢,哈哈)。
先对writeObject进行分析:
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
//获取当前实例的位桶大小
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
//调用ObjectOutputStream的默认序列化方法
s.defaultWriteObject();
//序列化位桶
s.writeInt(buckets);
//序列化当前对象的大小
s.writeInt(size);
//序列化每个entry内的数据,即每个位桶内的K-V
internalWriteEntries(s);
}
其中:
//瞬态的size
transient int size;
//获取当前实例的位桶容量
final int capacity() {
return (table != null) ? table.length :
(threshold > 0) ? threshold :
DEFAULT_INITIAL_CAPACITY;
}
//遍历所有位桶,逐个序列化位桶内的K-V
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
Node[] tab;
if (size > 0 && (tab = table) != null) {
for (int i = 0; i < tab.length; ++i) {
for (Node e = tab[i]; e != null; e = e.next) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}
}
}
从上面代码可以看出,hashmap将位桶的数据用transient进行修饰:
transient Node[] table;
进行这样修饰之后,也不是说在进行序列化时忽略掉,而是基于一种优化思想:在进行序列化时,如果该位桶有数据,我就进行序列化,如果没有数据,那么我就不进行序列化。
再对readObject进行分析:
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
//调用ObjectInputStream内的默认方法进行反序列化
s.defaultReadObject();
//类似于进行反构造
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " + loadFactor);
//对于这个丢弃处理表示很不解
s.readInt();
//读取需进行反序列化的实例大小
int mappings = s.readInt();
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " + mappings);
else if (mappings > 0) {
// range of 0.25...4.0
//随机生成一个类似于负载因子的数
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
//生成位桶大小
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
//生成阈值
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
@SuppressWarnings({"rawtypes","unchecked"})
Node[] tab = (Node[])new Node[cap];
table = tab;
//遍历,逐个反序列化
//从这也可以看出,在进行反序列化的时候,不保证每个位桶内的数据顺序与原来的保持一致
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
8、小结
随着技术的发展,如今的序列化已经不限于java原生的序列化机制了.如果想了解javaweb中的json序列化,可以看这个文章了解一下,不过目前的使用已经远超这篇文章的叙述了:
http://www.ibm.com/developerworks/cn/web/wa-lo-json/?ca=drs-tp3308
注:部分代码与思想来自于Thinking in java(fourth edition)