news 2026/5/15 20:42:33

Java——标准序列化机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java——标准序列化机制

标准序列化机制

    • 1、基本用法
    • 2、复杂对象
    • 3、定制序列化
    • 4、序列化的基本原理
    • 5、版本问题
    • 6、序列化特点分析

1、基本用法

要让一个类支持序列化,只需要让这个类实现接口java.io.Serializable。Serializable没有定义任何方法,只是一个标记接口。比如,对于前面章节提到的Student类,为支持序列化,可改为:

publicclassStudentimplementsSerializable{//省略主体代码}

声明实现了Serializable接口后,保存/读取Student对象就可以使用ObjectOutput-Stream/ObjectInputStream流了。

ObjectOutputStream是OutputStream的子类,但实现了Object-Output接口。ObjectOutput是DataOutput的子接口,增加了一个方法:

publicvoidwriteObject(Objectobj)throwsIOException

这个方法能够将对象obj转化为字节,写到流中。

ObjectInputStream是InputStream的子类,它实现了ObjectInput接口。ObjectInput是DataInput的子接口,增加了一个方法:

publicObjectreadObject()throwsClassNotFoundException,IOException

这个方法能够从流中读取字节,转化为一个对象。

使用这两个流,保存学生列表的代码就可以变为:

publicstaticvoidwriteStudents(List<Student>students)throwsIOException{ObjectOutputStreamout=newObjectOutputStream(newBufferedOutputStream(newFileOutputStream("students.dat")));try{out.writeInt(students.size());for(Students:students){out.writeObject(s);}}finally{out.close();}}

而从文件中读入学生列表的代码可以变为:

publicstaticList<Student>readStudents()throwsIOException,ClassNotFoundException{ObjectInputStreamin=newObjectInputStream(newBufferedInputStream(newFileInputStream("students.dat")));try{intsize=in.readInt();List<Student>list=newArrayList<>(size);for(inti=0;i<size;i++){list.add((Student)in.readObject());}returnlist;}finally{in.close();}}

实际上,只要List对象也实现了Serializable(ArrayList/LinkedList都实现了)​,上面代码还可以进一步简化,读写只需要一行代码,如下所示:

publicstaticvoidwriteStudents(List<Student>students)throwsIOException{ObjectOutputStreamout=newObjectOutputStream(newBufferedOutputStream(newFileOutputStream("students.dat")));try{out.writeObject(students);}finally{out.close();}}publicstaticList<Student>readStudents()throwsIOException,ClassNotFoundException{ObjectInputStreamin=newObjectInputStream(newBufferedInputStream(newFileInputStream("students.dat")));try{return(List<Student>)in.readObject();}finally{in.close();}}

是不是很神奇?只要将类声明实现Serializable接口,然后就可以使用ObjectOutput-Stream/ObjectInputStream直接读写对象了。我们之前介绍的各种类,如String、Date、Double、ArrayList、LinkedList、HashMap、TreeMap等,都实现了Serializable。

2、复杂对象

上面例子中的Student对象是非常简单的,如果对象比较复杂呢?比如:

  1. 如果a、b两个对象都引用同一个对象c,序列化后c是保存两份还是一份?在反序列化后还能让a、b指向同一个对象吗?答案是,c只会保存一份,反序列化后指向相同对象。
  2. 如果a、b两个对象有循环引用呢?即a引用了b,而b也引用了a。这种情况Java也没问题,可以保持引用关系。

这就是Java序列化机制的神奇之处,它能自动处理引用同一个对象的情况,也能自动处理循环引用的情况,具体例子我们就不介绍了。

3、定制序列化

默认的序列化机制已经很强大了,它可以自动将对象中的所有字段自动保存和恢复,但这种默认行为有时候不是我们想要的。

对于有些字段,它的值可能与内存位置有关,比如默认的hashCode()方法的返回值,当恢复对象后,内存位置肯定变了,基于原内存位置的值也就没有了意义。还有一些字段,可能与当前时间有关,比如表示对象创建时的时间,保存和恢复这个字段就是不正确的。

还有一些情况,如果类中的字段表示的是类的实现细节,而非逻辑信息,那默认序列化也是不适合的。为什么不适合呢?因为序列化格式表示一种契约,应该描述类的逻辑结构,而非与实现细节相绑定,绑定实现细节将使得难以修改,破坏封装。

比如,我们在容器类中介绍的LinkedList,它的默认序列化就是不适合的。为什么呢?因为LinkedList表示一个List,它的逻辑信息是列表的长度,以及列表中的每个对象,但LinkedList类中的字段表示的是链表的实现细节,如头尾节点指针,对每个节点,还有前驱和后继节点指针等。

那怎么办呢?Java提供了多种定制序列化的机制,主要的有两种:

  • 一种是transient关键字
  • 另外一种是实现writeObject和readObject方法。

将字段声明为transient,默认序列化机制将忽略该字段,不会进行保存和恢复。比如,类LinkedList中,它的字段都声明为了transient,如下所示:

transientintsize=0;transientNode<E>first;transientNode<E>last;

声明为了transient,不是说就不保存该字段了,而是告诉Java默认序列化机制,不要自动保存该字段了,可以实现writeObject/readObject方法来自己保存该字段。

类可以实现writeObject方法,以自定义该类对象的序列化过程,其声明必须为:

privatevoidwriteObject(java.io.ObjectOutputStreams)throwsjava.io.IOException

可以在这个方法中,调用ObjectOutputStream的方法向流中写入对象的数据。比如, LinkedList使用如下代码序列化列表的逻辑数据:

privatevoidwriteObject(java.io.ObjectOutputStreams)throwsjava.io.IOException{s.defaultWriteObject();//写元素个数s.writeInt(size);//循环写每个元素for(Node<E>x=first;x!=null;x=x.next)s.writeObject(x.item);}

需要注意的是代码:

s.defaultWriteObject();

这一行是必需的,它会调用默认的序列化机制,默认机制会保存所有没声明为transient的字段,即使类中的所有字段都是transient,也应该写这一行,因为Java的序列化机制不仅会保存纯粹的数据信息,还会保存一些元数据描述等隐藏信息,这些隐藏的信息是序列化之所以能够神奇的重要原因。

与writeObject对应的是readObject方法,通过它自定义反序列化过程,其声明必须为:

privatevoidreadObject(java.io.ObjectInputStreams)throwsjava.io.IOException,ClassNotFoundException

在这个方法中,调用ObjectInputStream的方法从流中读入数据,然后初始化类中的成员变量。比如,LinkedList的反序列化代码为:

privatevoidreadObject(java.io.ObjectInputStreams)throwsjava.io.IOException,ClassNotFoundException{s.defaultReadObject();//读元素个数intsize=s.readInt();//循环读入每个元素for(inti=0;i<size;i++)linkLast((E)s.readObject());}

注意代码:

s.defaultReadObject();

这一行代码也是必需的。

除了自定义writeObject/readObject方法,还有一些自定义序列化过程的机制:Exter-nalizable接口、readResolve方法和writeReplace方法,这些机制用得相对较少,我们就不介绍了。

4、序列化的基本原理

  1. 如果类的字段表示的就是类的逻辑信息,如上面的Student类,那就可以使用默认序列化机制,只要声明实现Serializable接口即可。
  2. 否则的话,如LinkedList,那就可以使用transient关键字,实现writeObject和read-Object自定义序列化过程。
  3. Java的序列化机制可以自动处理如引用同一个对象、循环引用等情况。

序列化到底是如何发生的呢?关键在ObjectOutputStream的writeObject和ObjectInput-Stream的readObject方法内。它们的实现都非常复杂,正因为这些复杂的实现才使得序列化看上去很神奇,我们简单介绍其基本逻辑。

writeObject的基本逻辑是:

  1. 如果对象没有实现Serializable,抛出异常NotSerializableException。
  2. 每个对象都有一个编号,如果之前已经写过该对象了,则本次只会写该对象的引用,这可以解决对象引用和循环引用的问题。
  3. 如果对象实现了writeObject方法,调用它的自定义方法。
  4. 默认是利用反射机制,遍历对象结构图,对每个没有标记为transient的字段,根据其类型,分别进行处理,写出到流,流中的信息包括字段的类型,即完整类名、字段名、字段值等。

readObject的基本逻辑是:

  • 不调用任何构造方法;
  • 它自己就相当于是一个独立的构造方法,根据字节流初始化对象,利用的也是反射机制;
  • 在解析字节流时,对于引用到的类型信息,会动态加载,如果找不到类,会抛出ClassNotFoundException。

5、版本问题

代码是在不断演化的,而序列化的对象可能是持久保存在文件上的,如果类的定义发生了变化,那持久化的对象还能反序列化吗?

默认情况下,Java会给类定义一个版本号,这个版本号是根据类中一系列的信息自动生成的。在反序列化时,如果类的定义发生了变化,版本号就会变化,与流中的版本号就会不匹配,反序列化就会抛出异常,类型为java.io.InvalidClassException。

通常情况下,我们希望自定义这个版本号,而非让Java自动生成,一方面是为了更好地控制,另一方面是为了性能,因为Java自动生成的性能比较低。怎么自定义呢?在类中定义如下变量:

privatestaticfinallongserialVersionUID=1L;

在Java IDE如Eclipse中,如果声明实现了Serializable而没有定义该变量,IDE会提示自动生成。这个变量的值可以是任意的,代表该类的版本号。在序列化时,会将该值写入流,在反序列化时,会将流中的值与类定义中的值进行比较,如果不匹配,会抛出InvalidClassException。

那如果版本号一样,但实际的字段不匹配呢?Java会分情况自动进行处理,以尽量保持兼容性,大概分为三种情况:

  • 字段删掉了:即流中有该字段,而类定义中没有,该字段会被忽略;
  • 新增了字段:即类定义中有,而流中没有,该字段会被设为默认值;
  • 字段类型变了:对于同名的字段,类型变了,会抛出InvalidClassException。

6、序列化特点分析

序列化的主要用途有两个:一个是对象持久化;另一个是跨网络的数据交换、远程过程调用。Java标准的序列化机制有很多优点,使用简单,可自动处理对象引用和循环引用,也可以方便地进行定制,处理版本问题等,但它也有一些重要的局限性。

  1. Java序列化格式是一种私有格式,是一种Java特有的技术,不能被其他语言识别,不能实现跨语言的数据交换。
  2. Java在序列化字节中保存了很多描述信息,使得序列化格式比较大。
  3. Java的默认序列化使用反射分析遍历对象结构,性能比较低。
  4. Java的序列化格式是二进制的,不方便查看和修改。

由于这些局限性,实践中往往会使用一些替代方案。在跨语言的数据交换格式中,ⅩML/JSON是被广泛采用的文本格式,各种语言都有对它们的支持,文件格式清晰易读。有很多查看和编辑工具,它们的不足之处是性能和序列化大小,在性能和大小敏感的领域,往往会采用更为精简高效的二进制方式,如ProtoBuf、Thrift、MessagePack等。

至此,关于Java的标准序列化机制就介绍完了。我们介绍了它的用法和基本原理,最后分析了它的特点,它是一种神奇的机制,通过简单的Serializable接口就能自动处理很多复杂的事情,但它也有一些重要的限制,最重要的是不能跨语言。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/15 20:41:26

AI应用Docker镜像实战:从拉取部署到生产优化全解析

1. 项目概述&#xff1a;从开源镜像到AI应用部署的实战解析 最近在部署一些AI应用时&#xff0c;经常需要拉取一些特定的开源模型或工具镜像。 windagency/valora.ai 这个镜像名&#xff0c;对于不少在AI应用开发和部署一线的朋友来说&#xff0c;可能并不陌生。它不像那些耳…

作者头像 李华
网站建设 2026/5/15 20:41:25

【MATLAB】流量控制系统非线性补偿仿真与调试

【MATLAB】流量控制系统非线性补偿仿真与调试 摘要:流量控制是工业生产、能源供给、水利工程等领域的核心工艺环节,广泛应用于化工管路输送、给排水系统、热力供应、精密流体控制等场景,其控制精度与稳定性直接决定生产效率、产品质量及系统运行经济性。工业流量控制系统普…

作者头像 李华
网站建设 2026/5/15 20:40:23

在ubuntu系统上使用curl快速接入taotoken多模型api服务

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 在 Ubuntu 系统上使用 curl 快速接入 Taotoken 多模型 API 服务 对于习惯在终端环境下工作的开发者而言&#xff0c;curl 是一个验…

作者头像 李华
网站建设 2026/5/15 20:39:14

从飞思卡尔智能车大赛看嵌入式系统开发:感知、决策与控制实战

1. 项目概述&#xff1a;一场定义时代的嵌入式竞赛十多年前&#xff0c;当“智能车”这个词对大多数人还停留在科幻电影里时&#xff0c;一群大学生已经用电路板、传感器和代码&#xff0c;让一辆辆巴掌大的模型车在赛道上风驰电掣&#xff0c;自主完成循迹、避障、超车。2010年…

作者头像 李华
网站建设 2026/5/15 20:38:19

OpenClaw-RUH:基于深度学习的机器人灵巧抓取框架解析与实践

1. 项目概述&#xff1a;当AI遇上“机械爪”最近在AI和机器人交叉的圈子里&#xff0c;一个名为“OpenClaw-RUH”的项目引起了我的注意。乍一看这个标题&#xff0c;你可能会觉得它又是一个开源的机械臂控制项目。但当我深入其代码仓库和社区讨论后&#xff0c;发现它的野心远不…

作者头像 李华