当前位置: 首页 > news >正文

Java 序列化:深入探索 Serializable 接口

在 Java 编程中,序列化是一个非常重要的概念,它允许我们将对象转换为字节流,以便于存储或传输。反序列化则是将字节流转换回对象的过程。尽管我们通常只需要让类实现 Serializable 接口即可实现序列化,但深入了解其背后的机制和细节,对于提升我们的编程技能和理解 Java 的内部工作原理非常有帮助。

1 理论基础

Java 序列化是在 JDK 1.1 中引入的特性,用于将 Java 对象转换为字节数组,便于存储或传输。反序列化则是将字节数组转换回 Java 对象的过程。

序列化的思想是“冻结”对象状态,然后写到磁盘或者在网络中传输;反序列化的思想是“解冻”对象状态,重新获得可用的 Java 对象。

要序列化的对象必须实现 Serializable 接口,否则会抛出 NotSerializableException 异常。

Serializable 接口定义

public interface Serializable {
}

Serializable 接口是一个空接口,没有任何方法。它的作用仅仅是作为一个标识,告诉 Java 虚拟机该类的对象是可以被序列化的。

2 实战演练

首先,我们创建一个简单的类 Wanger,包含两个字段 nameage,并实现 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 + "}";}
}

接下来,我们创建一个测试类,通过 ObjectOutputStreamWanger 对象序列化到文件中,再通过 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}

从结果中可以看出:

  1. static 字段 pre 在反序列化后保持了修改后的值,说明 static 字段不会被序列化。
  2. 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 接口的类需要手动实现 writeExternalreadExternal 方法,以便在序列化和反序列化过程中对对象进行自定义的处理。

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 区别与使用场景

  1. 无参构造方法

    Serializable:不需要无参构造方法。

    Externalizable:必须提供一个无参构造方法。在反序列化时,会调用该无参构造方法创建对象,然后再将被保存对象的字段值复制过去。

  2. 方法实现

    Serializable:不需要实现任何方法,序列化和反序列化过程由 Java 自动处理。

    Externalizable:需要实现 writeExternal 和 readExternal 方法,手动控制序列化和反序列化的过程。

  3. 控制能力

    Serializable:自动进行序列化和反序列化,无法对序列化过程进行细粒度控制。

    Externalizable:提供了更高的控制能力,可以在序列化和反序列化过程中对对象进行自定义处理,如对敏感信息进行加密和解密。

  4. 性能

    Serializable:由于是自动进行的,性能相对较低。

    Externalizable:由于是手动控制的,性能相对较高,但需要更多的编码工作。

6 serialVersionUID 的作用及选择

6.1 serialVersionUID 的作用

serialVersionUID 被称为序列化 ID,它是决定 Java 对象能否反序列化成功的重要因子。在反序列化时,Java 虚拟机会把字节流中的 serialVersionUID 与被序列化类中的 serialVersionUID 进行比较,如果相同则可以进行反序列化,否则就会抛出序列化版本不一致的异常。

6.2 生成 serialVersionUID 的方式

当一个类实现了 Serializable 接口后,IDE 通常会提醒该类最好产生一个序列化 ID。生成 serialVersionUID 的方式有以下几种:

  1. 默认版本的序列化 ID

    private static final long serialVersionUID = 1L;
    
  2. 随机生成的不重复的序列化 ID

    private static final long serialVersionUID = -2095916884810199532L;
    
  3. 使用 @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 的方式有三种:

  1. 默认版本的序列化 IDprivate static final long serialVersionUID = 1L;
  2. 随机生成的不重复的序列化 IDprivate static final long serialVersionUID = -2095916884810199532L;
  3. 使用 @SuppressWarnings("serial") 注解@SuppressWarnings("serial")

如果没有特殊需求,推荐使用默认的序列化 ID(1L),这样可以确保代码一致时反序列化成功。

7 总结

通过深入研究 Java 序列化机制,我们不仅理解了 Serializable 接口的作用,还掌握了 Externalizable 接口的使用方法,以及 serialVersionUID 的重要性。这些知识将帮助我们在实际开发中更好地处理对象的序列化和反序列化问题,提升代码的健壮性和可维护性。

8 思维导图

在这里插入图片描述

9 参考链接

Java Serializable 接口:明明就一个空的接口嘛


http://www.mrgr.cn/news/66019.html

相关文章:

  • DAY18|二叉树Part06|LeetCode: 530.二叉搜索树的最小绝对差、501. 二叉搜索树中的众数、236.二叉树的最近公共祖先
  • LeetCode:633. 平方数之和(Java)
  • mysql笔记-日志
  • WorkFlow源码剖析——Communicator之TCPServer(中)
  • .net core 接口,动态接收各类型请求的参数
  • 提高交换网络可靠性之链路聚合
  • SpringBoot在线教育系统:安全与维护
  • Rust数据NoSQL 数据库的使用
  • 蒙特卡洛树搜索(MCTS)
  • 虚拟仿真平台在中医针灸实训室实验课程中的应用
  • 【ARM Linux 系统稳定性分析入门及渐进 1.9.1 -- Crash 命令 System State 集合】
  • Wecom酱搭建企业微信发送消息
  • 华为OD机试 - 删除字符串中出现次数最少的字符 (Java 2024 E卷 100分)
  • 手把手教程:使用 Fluentbit 采集夜莺日志写入 ElasticSearch
  • 目前的AI 到底是背出了答案,还是推理出了答案?
  • 计算生物学与生物信息学漫谈-5-mapping算法
  • 集合数据结构之哈希集、有序集合
  • VS警告C26440:函数可以声明为noexcept
  • 征程 6E DISPLAY 功能介绍及上手实践
  • 医疗行业的AI革命:机器人护理,你准备好了吗
  • YOLOv10改进策略【卷积层】| ECCV-2024 Histogram Transformer 直方图自注意力 适用于噪声大,图像质量低的检测任务
  • Hadoop完全分布式环境搭建步骤
  • Uni-App全局文件执行顺序详解
  • ThinkRAG开源!笔记本电脑可运行的本地知识库大模型检索增强生成系统
  • python - leetcode【数据结构-算法】-入门/通关手册
  • @ApiOperation该注解的用法