Java 序列化:深入探索 Serializable 接口
在 Java 编程中,序列化是一个非常重要的概念,它允许我们将对象转换为字节流,以便于存储或传输。反序列化则是将字节流转换回对象的过程。尽管我们通常只需要让类实现 Serializable
接口即可实现序列化,但深入了解其背后的机制和细节,对于提升我们的编程技能和理解 Java 的内部工作原理非常有帮助。
1 理论基础
Java 序列化是在 JDK 1.1 中引入的特性,用于将 Java 对象转换为字节数组,便于存储或传输。反序列化则是将字节数组转换回 Java 对象的过程。
序列化的思想是“冻结”对象状态,然后写到磁盘或者在网络中传输;反序列化的思想是“解冻”对象状态,重新获得可用的 Java 对象。
要序列化的对象必须实现 Serializable
接口,否则会抛出 NotSerializableException
异常。
Serializable 接口定义
public interface Serializable {
}
Serializable
接口是一个空接口,没有任何方法。它的作用仅仅是作为一个标识,告诉 Java 虚拟机该类的对象是可以被序列化的。
2 实战演练
首先,我们创建一个简单的类 Wanger
,包含两个字段 name
和 age
,并实现 Serializable
接口。
class Wanger implements Serializable {private static final long serialVersionUID = -2095916884810199532L;private String name;private int age;public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "Wanger{" + "name=" + name + ",age=" + age + "}";}
}
接下来,我们创建一个测试类,通过 ObjectOutputStream
将 Wanger
对象序列化到文件中,再通过 ObjectInputStream
从文件中反序列化对象。
public class Test {public static void main(String[] args) {// 初始化Wanger wanger = new Wanger();wanger.setName("王二");wanger.setAge(18);System.out.println(wanger);// 把对象写到文件中try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));) {oos.writeObject(wanger);} catch (IOException e) {e.printStackTrace();}// 从文件中读出对象try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));) {Wanger wanger1 = (Wanger) ois.readObject();System.out.println(wanger1);} catch (IOException | ClassNotFoundException e) {e.printStackTrace();}}
}
3 序列化与反序列化的内部机制
ObjectOutputStream
在序列化对象时,会依次调用 writeObject()
→ writeObject0()
→ writeOrdinaryObject()
→ writeSerialData()
→ invokeWriteObject()
→ defaultWriteFields()
。
ObjectInputStream
在反序列化对象时,会依次调用 readObject()
→ readObject0()
→ readOrdinaryObject()
→ readSerialData()
→ defaultReadFields()
。
3.1 defaultWriteFields() 方法
private void defaultWriteFields(Object obj, ObjectStreamClass desc) throws IOException {// 获取对象的类,并检查是否可以进行默认的序列化Class<?> cl = desc.forClass();desc.checkDefaultSerialize();// 获取对象的基本类型字段的数量,以及这些字段的值int primDataSize = desc.getPrimDataSize();desc.getPrimFieldValues(obj, primVals);// 将基本类型字段的值写入输出流bout.write(primVals, 0, primDataSize, false);// 获取对象的非基本类型字段的值ObjectStreamField[] fields = desc.getFields(false);Object[] objVals = new Object[desc.getNumObjFields()];int numPrimFields = fields.length - objVals.length;desc.getObjFieldValues(obj, objVals);// 循环写入对象的非基本类型字段的值for (int i = 0; i < objVals.length; i++) {// 调用 writeObject0 方法将对象的非基本类型字段序列化写入输出流try {writeObject0(objVals[i], fields[numPrimFields + i].isUnshared());}// 如果在写入过程中出现异常,则将异常包装成 IOException 抛出catch (IOException ex) {if (abortIOException == null) {abortIOException = ex;}}}
}
3.2 defaultReadFields() 方法
private void defaultReadFields(Object obj, ObjectStreamClass desc) throws IOException {// 获取对象的类,并检查对象是否属于该类Class<?> cl = desc.forClass();if (cl != null && obj != null && !cl.isInstance(obj)) {throw new ClassCastException();}// 获取对象的基本类型字段的数量和值int primDataSize = desc.getPrimDataSize();if (primVals == null || primVals.length < primDataSize) {primVals = new byte[primDataSize];}// 从输入流中读取基本类型字段的值,并存储在 primVals 数组中bin.readFully(primVals, 0, primDataSize, false);if (obj != null) {// 将 primVals 数组中的基本类型字段的值设置到对象的相应字段中desc.setPrimFieldValues(obj, primVals);}// 获取对象的非基本类型字段的数量和值int objHandle = passHandle;ObjectStreamField[] fields = desc.getFields(false);Object[] objVals = new Object[desc.getNumObjFields()];int numPrimFields = fields.length - objVals.length;// 循环读取对象的非基本类型字段的值for (int i = 0; i < objVals.length; i++) {// 调用 readObject0 方法读取对象的非基本类型字段的值ObjectStreamField f = fields[numPrimFields + i];objVals[i] = readObject0(Object.class, f.isUnshared());// 如果该字段是一个引用字段,则将其标记为依赖该对象if (f.getField() != null) {handles.markDependency(objHandle, passHandle);}}if (obj != null) {// 将 objVals 数组中的非基本类型字段的值设置到对象的相应字段中desc.setObjFieldValues(obj, objVals);}passHandle = objHandle;
}
4 注意事项
- static 和 transient 修饰的字段不会被序列化。
4.1 代码示例
class Wanger implements Serializable {private static final long serialVersionUID = -2095916884810199532L;private String name;private int age;public static String pre = "沉默";transient String meizi = "王三";@Overridepublic String toString() {return "Wanger{" + "name=" + name + ",age=" + age + ",pre=" + pre + ",meizi=" + meizi + "}";}
}
public class Test {public static void main(String[] args) {// 初始化Wanger wanger = new Wanger();wanger.setName("王二");wanger.setAge(18);System.out.println(wanger);// 把对象写到文件中try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));) {oos.writeObject(wanger);} catch (IOException e) {e.printStackTrace();}// 改变 static 字段的值Wanger.pre = "不沉默";// 从文件中读出对象try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));) {Wanger wanger1 = (Wanger) ois.readObject();System.out.println(wanger1);} catch (IOException | ClassNotFoundException e) {e.printStackTrace();}}
}
输出结果:
Wanger{name=王二,age=18,pre=沉默,meizi=王三}
Wanger{name=王二,age=18,pre=不沉默,meizi=null}
从结果中可以看出:
static
字段pre
在反序列化后保持了修改后的值,说明static
字段不会被序列化。transient
字段meizi
在反序列化后为null
,说明transient
字段也不会被序列化。
4.2 源码探究
如果想要深究源码的话,你可以在 ObjectStreamClass 中发现下面这样的代码:
private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {// 获取该类中声明的所有字段Field[] clFields = cl.getDeclaredFields();ArrayList<ObjectStreamField> list = new ArrayList<>();int mask = Modifier.STATIC | Modifier.TRANSIENT;// 遍历所有字段,将非 static 和 transient 的字段添加到 list 中for (int i = 0; i < clFields.length; i++) {Field field = clFields[i];int mods = field.getModifiers();if ((mods & mask) == 0) {// 根据字段名、字段类型和字段是否可序列化创建一个 ObjectStreamField 对象ObjectStreamField osf = new ObjectStreamField(field.getName(), field.getType(), !Serializable.class.isAssignableFrom(cl));list.add(osf);}}int size = list.size();// 如果 list 为空,则返回一个空的 ObjectStreamField 数组,否则将 list 转换为 ObjectStreamField 数组并返回return (size == 0) ? NO_FIELDS :list.toArray(new ObjectStreamField[size]);
}
其中,Modifier.STATIC | Modifier.TRANSIENT
表示这两个修饰符标记的字段就没有被放入到序列化的字段中。
5 Externalizable 接口
除了 Serializable
接口,Java 还提供了 Externalizable
接口,它提供了更高的序列化控制能力。
Externalizable
接口的类需要手动实现 writeExternal
和 readExternal
方法,以便在序列化和反序列化过程中对对象进行自定义的处理。
5.1 代码示例
class Wanger implements Externalizable {private String name;private int age;public Wanger() {// 必须提供一个无参构造方法}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "Wanger{" + "name=" + name + ",age=" + age + "}";}@Overridepublic void writeExternal(ObjectOutput out) throws IOException {out.writeObject(name);out.writeInt(age);}@Overridepublic void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {name = (String) in.readObject();age = in.readInt();}
}
public class Test {public static void main(String[] args) {// 初始化Wanger wanger = new Wanger();wanger.setName("王二");wanger.setAge(18);System.out.println(wanger);// 把对象写到文件中try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));) {oos.writeObject(wanger);} catch (IOException e) {e.printStackTrace();}// 从文件中读出对象try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));) {Wanger wanger1 = (Wanger) ois.readObject();System.out.println(wanger1);} catch (IOException | ClassNotFoundException e) {e.printStackTrace();}}
}
输出结果:
Wanger{name=王二,age=18}
Wanger{name=王二,age=18}
5.2 区别与使用场景
-
无参构造方法
Serializable
:不需要无参构造方法。Externalizable
:必须提供一个无参构造方法。在反序列化时,会调用该无参构造方法创建对象,然后再将被保存对象的字段值复制过去。 -
方法实现
Serializable
:不需要实现任何方法,序列化和反序列化过程由 Java 自动处理。Externalizable
:需要实现 writeExternal 和 readExternal 方法,手动控制序列化和反序列化的过程。 -
控制能力
Serializable
:自动进行序列化和反序列化,无法对序列化过程进行细粒度控制。Externalizable
:提供了更高的控制能力,可以在序列化和反序列化过程中对对象进行自定义处理,如对敏感信息进行加密和解密。 -
性能
Serializable
:由于是自动进行的,性能相对较低。Externalizable
:由于是手动控制的,性能相对较高,但需要更多的编码工作。
6 serialVersionUID 的作用及选择
6.1 serialVersionUID
的作用
serialVersionUID
被称为序列化 ID,它是决定 Java 对象能否反序列化成功的重要因子。在反序列化时,Java 虚拟机会把字节流中的 serialVersionUID
与被序列化类中的 serialVersionUID
进行比较,如果相同则可以进行反序列化,否则就会抛出序列化版本不一致的异常。
6.2 生成 serialVersionUID
的方式
当一个类实现了 Serializable
接口后,IDE 通常会提醒该类最好产生一个序列化 ID。生成 serialVersionUID
的方式有以下几种:
-
默认版本的序列化 ID:
private static final long serialVersionUID = 1L;
-
随机生成的不重复的序列化 ID:
private static final long serialVersionUID = -2095916884810199532L;
-
使用
@SuppressWarnings("serial")
注解:@SuppressWarnings("serial")
6.3 选择 serialVersionUID
的方式
6.3.1 使用随机生成的序列化 ID
首先,我们采用第二种办法,在被序列化类中添加一个随机生成的序列化 ID。
class Wanger implements Serializable {private static final long serialVersionUID = -2095916884810199532L;private String name;private int age;// 其他代码忽略
}
然后,序列化一个 Wanger
对象到文件中。
// 初始化
Wanger wanger = new Wanger();
wanger.setName("王二");
wanger.setAge(18);
System.out.println(wanger);// 把对象写到文件中
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));) {oos.writeObject(wanger);
} catch (IOException e) {e.printStackTrace();
}
这时候,我们悄悄地把 Wanger
类的序列化 ID 修改一下。
// private static final long serialVersionUID = -2095916884810199532L;
private static final long serialVersionUID = -2095916884810199533L;
准备反序列化。
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));) {Wanger wanger = (Wanger) ois.readObject();System.out.println(wanger);
} catch (IOException | ClassNotFoundException e) {e.printStackTrace();
}
结果会抛出异常:
java.io.InvalidClassException: local class incompatible: stream classdesc
serialVersionUID = -2095916884810199532,
local class serialVersionUID = -2095916884810199533
异常堆栈信息告诉我们,从持久化文件里面读取到的序列化 ID 和本地的序列化 ID 不一致,无法反序列化。
6.3.2 使用 @SuppressWarnings("serial")
注解
假如我们采用第三种方法,为 Wanger
类添加 @SuppressWarnings("serial")
注解。
@SuppressWarnings("serial")
class Wanger implements Serializable {// 省略其他代码
}
再来一次反序列化,结果依然报错。
java.io.InvalidClassException: local class incompatible: stream classdesc
serialVersionUID = -2095916884810199532,
local class serialVersionUID = -3818877437117647968
异常堆栈信息告诉我们,本地的序列化 ID 为 -3818877437117647968
,和持久化文件里面读取到的序列化 ID 仍然不一致,无法反序列化。这说明使用 @SuppressWarnings("serial")
注解时,该注解会为被序列化类自动生成一个随机的序列化 ID。
6.3.3 使用默认的序列化 ID
如果没有特殊需求,采用默认的序列化 ID(1L)就可以,这样可以确保代码一致时反序列化成功。
class Wanger implements Serializable {private static final long serialVersionUID = 1L;// 省略其他代码
}
6.4 小结
serialVersionUID
是决定 Java 对象能否反序列化成功的重要因子。在反序列化时,Java 虚拟机会比较字节流中的 serialVersionUID
和类中的 serialVersionUID
,如果不一致,则会抛出 InvalidClassException
。
生成 serialVersionUID
的方式有三种:
- 默认版本的序列化 ID:
private static final long serialVersionUID = 1L;
- 随机生成的不重复的序列化 ID:
private static final long serialVersionUID = -2095916884810199532L;
- 使用
@SuppressWarnings("serial")
注解:@SuppressWarnings("serial")
如果没有特殊需求,推荐使用默认的序列化 ID(1L),这样可以确保代码一致时反序列化成功。
7 总结
通过深入研究 Java 序列化机制,我们不仅理解了 Serializable
接口的作用,还掌握了 Externalizable
接口的使用方法,以及 serialVersionUID
的重要性。这些知识将帮助我们在实际开发中更好地处理对象的序列化和反序列化问题,提升代码的健壮性和可维护性。
8 思维导图
9 参考链接
Java Serializable 接口:明明就一个空的接口嘛