一、序列化实现方式之实现Serializable接口
1.Serializable接口的作用
serializable接口就是一个标识,实现了serializable接口的类就相当于打上了一个tag,有这个tag的类就可以被序列化,如果没有实现Serializable接口在被序列化的时候就会抛出NotSerializableException异常:
ObjectOutPutStream源码中的writeObject0方法:
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
...
// remaining cases
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());
}
}
} finally {
depth--;
bout.setBlockDataMode(oldMode);
}
}
在序列化对象的时候,会获取对象中所有的属性,然后循环把对象的属性也一同序列化,如果对象的属性中没有实现Serializable属性也会抛出 NotSerializableException异常。
2. 实现Serializable接口后,具体是如何序列化的
序列化本质是把内存中的对象通过IO流的方式保存在磁盘中,所以做序列化的工作的是ObjectOutPutStream,而ObjectOutPutStream把对象序列化的能力是通过实现ObjectOutPut接口。
3.ObjectOutPutStream之ObjectStreamClass
ObjectStreamClass用来描述被序列化的对象,例如对象是否是数组类型,是否是枚举类型,对象是否实现了Serializable接口、是否有writeObject()方法、是否有readObject()方法。
4.序列化之serialVersionUID
虚拟机是否允许反序列化,不仅取决于类路径(全类名必须一致)和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致。
SerialVersionUID 是一个标识符,它通常使用对象的哈希码,序列化时会标记在对象上。
可以通过 Java 中 serialver 工具找到该对象的 serialVersionUID,命令格式:serialver classname。
Java 序列化机制通过判断类的 serialVersionUID 验证版本的一致性。在进行反序列化时,Java 虚拟机会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是 InvalidCastException。
serialVersionUID 有两种显示的生成方式。
默认为 1L,例如 private static final long serialVersionUID = 1L;
根据类名、接口名、成员方法及属性等来生成一个 64 位的哈希字段,随机生成的序列化 ID,通过改变序列化 ID 可以用来限制某些用户的使用。
当实现 java.io.Serializable 接口的类没有显式地定义一个 serialVersionUID 变量时候,Java 序列化机制会根据编译的 Class 自动生成一个 serialVersionUID 作为序列化版本比较使用。这种情况下,如果 Class 文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID 也不会变化。
5.Transient关键字
(1).Transient关键字作用
Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中。
在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
(2).Transient 与 Static
静态变量不是对象状态的一部分,因此它不参与序列化。
将静态变量声明为 transient 变量没有用处。
(3).Final 与 Transient
final 变量将直接通过值参与序列化,将 final 变量声明为 transient 变量不会产生任何影响。
6.writeObject和readObject方法
如果想自己控制序列化的,可以实现这两个方法:
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
aa = (String) s.readObject();
cc = s.readInt();
}
private void writeObject(ObjectOutputStream s) throws IOException {
s.writeObject(aa);
s.writeInt(cc);
}
注意一旦实现了这两个方法,序列化就会完全按照这两个方法去序列化,不会再走默认的序列化。
源码:
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;
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);
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
} finally {
curContext.setUsed();
curContext = oldContext;
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
curPut = oldPut;
} else {
defaultWriteFields(obj, slotDesc);
}
}
}
可以看到这个是通过slotDesc.hasWriteObjectMethod()来判断对象中是否有writeObject方法,slotDesc是ObjectStreamClass用来描述序列化对象的类,如果有的话就通过反射调用slotDesc.invokeWriteObject(obj, this);writeObject,readObject方法逻辑也是一样的。
7.如果子类实现了Serializable,父类没有实现Serializable接口
父类的字段不会进行序列化,当反序列化的时候,父类中的字段会是默认值,如果想让没有实现Serializable接口的父类的字段也能够序列化,可以使用readObject和writeObject方法。
8.如果父类实现了Serializable,那子类没有显示的实现Serializable接口
父类实现了Serializable,那子类会默认实现Serializable接口
9.如果防止序列化破坏单例模式
在单例的类中加入一个readResolve方法:
import java.io.Serializable;
public class Singleton implements Serializable {
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
private Object readResolve() {
return getSingleton();
}
}
// print true
readResolve方法:
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
...
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
是序列化反射中最后一个执行的方法,所以在这里做的操作会覆盖之前的操作。
二、序列化之Externalizable
1.注意点
(1).序列化的对象必须有默认的空构造方法
在反序列化的时候有一个判断:
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
...
return obj;
}
void checkDeserialize() throws InvalidClassException {
if (deserializeEx != null) {
throw deserializeEx.newInvalidClassException();
}
}
readClassDesc获取类的序列化描述对象ObjectStreamClass,然后通过ObjectStreamClass去检查是否有异常。
使用 Externalizable 进行序列化读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,再将被保存对象的字段的值分别填充到新对象中。所以,实现 Externalizable 接口的类必须 提供一个 public 的无参构造器。
(2).writeExternal和readExternal
这两个方法是Externalizable中的两个方法,writeExternal管理哪些属性从内存中写入磁盘,readExternal管理从磁盘中读入哪些属性,注意:读入的顺序必须和写出的顺序是一致的,可以少读,但是不能错读和多读,很符合文件流的读入写出规律
(3).transient关键字对于Externalizable不起作用
如果想让哪个字段不参加序列化,在writeExternal和readExternal的方法中不加入这个字段即可。