Java对象序列化全面总结

Java允许我们在内存中创建可复用的Java对象,但一般情况下,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象

Java对象序列化就能够帮助我们实现该功能。使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来再将这些字节组装成对象

必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量

除了在持久化对象时会用到对象序列化之外,当使用 RMI ,或在网络中传递对象时,都会用到对象序列化

Java序列化API为处理对象序列化提供了一个标准机制,该API简单易用,但性能不是最好的

在Java中,只要一个类实现了 java.io.Serializable 接口,它就可以被序列化( 枚举类可以被序列化)。

当重新读取被保存的Person对象时,并没有调用Person的任何构造器,看起来就像是直接使用字节将Person对象还原出来的。当Person对象被保存到person.out文件后,可以在其它地方去读取该文件以还原对象,但必须确保该读取程序的 CLASSPATH 中包含有 Person.class(哪怕在读取Person对象时并没有显示地使用Person类,如上例所示),否则会抛出 ClassNotFoundException。

简单的来说,Java 对象序列化就是把对象写入到输出流中,用来存储或传输;反序列化就是从输入流中读取对象。

序列化一个对象首先要创造某些OutputStream对象(如FileOutputStream、ByteArrayOutputStream等),然后将其封装在一个ObjectOutputStream对象中,在调用writeObject()方法即可序列化一个对象

反序列化的过程需要创造InputStream对象(如FileInputstream、ByteArrayInputStream等),然后将其封装在ObjectInputStream中,在调用readObject()即可

注意对象的序列化是基于字节的,不能使用基于字符的流。

使用ObjectOutputStream来持久化对象到文件中,使用了writeObject方法,该方法又调用了如下方法:

从上述代码可知,如果被写对象的类型是String,或数组,或Enum,或Serializable,那么就可以对该对象进行序列化,否则将抛出NotSerializableException。

即、String类型的对象、枚举类型的对象、数组对象,都是默认可以被序列化的。

如果仅仅让某个类实现Serializable接口,而没有其它任何处理的话,则就是使用默认序列化机制。

使用默认机制在序列化对象时,不仅会序列化当前对象,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化,以此类推。

所以,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大。

在现实应用中,有些时候不能使用默认序列化机制。比如,希望在序列化过程中忽略掉敏感数据,或者简化序列化过程。下面将介绍若干影响序列化的方法。

使用 transient 关键字

当类的某个字段被 transient 修饰,默认序列化机制就会忽略该字段。此处将Person类中的age字段声明为transient,如下所示

使用writeObject()方法与readObject()方法

对于上述已被声明为 transitive 的字段 age,除了将 transient 关键字去掉外,是否还有其它方法能使它再次可被序列化?

方法之一就是在Person类中添加两个方法:writeObject()与readObject(),如下所示:

必须注意地是,writeObject()与readObject()都是private方法,那么它们是如何被调用的呢?

毫无疑问,使用反射。详情可以看看ObjectOutputStream中的writeSerialData方法,以及ObjectInputStream中的readSerialData方法。这两个方法会在序列化、反序列化的过程中被自动调用。且不能关闭流,否则会导致序列化操作失败。

使用 Externalizable 接口

无论是使用 transient 关键字,还是使用 writeObject() 和 readObject() 方法,其实都是基于 Serializable 接口的序列化。

Java提供了另一个序列化接口 Externalizable,使用该接口之后,之前基于 Serializable 接口的序列化机制就将失效。Externalizable 接口继承于 Serializable 接口,当使用该接口时,序列化的细节需要由程序员去完成。将Person类作如下修改:

Externalizable 继承于 Serializable,当使用该接口时,序列化的细节需要由程序员去完成。

如上所示的代码,由于实现的writeExternal()与readExternal()方法未作任何处理,那么该序列化行为将不会保存/读取任何一个字段。这也就是为什么输出结果中所有字段的值均为空。

另外,使用 Externalizable 接口进行序列化时,读取对象会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中,这就是为什么在此次序列化过程中Person类的无参构造器会被调用。 由于这个原因,实现 Externalizable 接口的类必须要提供一个无参构造器,且它的访问权限为public。

对上述Person类做进一步的修改,使其能够对name与age字段进行序列化,但忽略 gender 字段:

当使用Singleton模式时,应该是期望某个类的实例应该是唯一的,但如果该类是可序列化的,那么情况可能略有不同。当然目前最好的单例实现方式是使用枚举,如果还是传统的实现方式,才会遇到这个问题。

能序列化的前提

如果一个类想被序列化,需要实现 Serializable 接口进行自动序列化,或者实现 Externalizable 接口进行手动序列化,否则强行序列化该类的对象,就会抛出 NotSerializableException 异常,这是因为,在序列化操作过程中会对类型进行检查,要求被序列化的类必须属于 Enum、Array 和 Serializable 类型其中的任何一种(Externalizable也继承了Serializable)。

JVM 是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)

transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

FileOutputStream 类有一个带有两个参数的重载 Constructor——FileOutputStream(String, boolean)。若其第二个参数为 true 且 String 代表的文件存在,那么将把新的内容写到原来文件的末尾而非重写这个文件,故不能用这个版本的构造函数来实现序列化,也就是说必须重写这个文件,否则在读取这个文件反序列化的过程中就会抛出异常,导致只有第一次写到这个文件中的对象可以被反序列化,之后程序就会出错。

要知道序列化的是什么样儿的对象(成员)

序列化并不保存静态变量

要想将父类对象也序列化,就需要让父类也实现 Serializable 接口

若一个类的字段有引用对象,那么在序列化该类的时候不仅该类要实现Serializable接口,这个引用类型也要实现Serializable接口。但有时我们并不需要对这个引用类型进行序列化,此时就需要使用transient关键字来修饰该引用类型保证在序列化的过程中跳过该引用类型。

通过序列化操作,可以实现对任何可 Serializable 对象的深度复制(deep copy),这意味着复制的是整个对象的关系网,而不仅仅是基本对象及其引用

如果父类没有实现Serializable接口,但其子类实现了此接口,那么这个子类是可以序列化的,但是在反序列化的过程中会调用父类的无参构造函数,所以在其直接父类(注意是直接父类)中必须有一个无参的构造函数。

序列化的安全性

服务器端给客户端发送序列化对象数据,序列化二进制格式的数据写在文档中,并且完全可逆。一抓包就能就看到类是什么样子,以及它包含什么内容。如果对象中有一些数据是敏感的,比如密码字符串等,则要对字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。

比如可以通过使用 writeObject 和 readObject 实现密码加密和签名管理,但其实还有更好的方式。

如果需要对整个对象进行加密和签名,最简单的是将它放在一个 javax.crypto.SealedObject 和/或 java.security.SignedObject 包装器中。两者都是可序列化的,所以将对象包装在 SealedObject 中可以围绕原对象创建一种 “包装盒”。必须有对称密钥才能解密,而且密钥必须单独管理。同样,也可以将 SignedObject 用于数据验证,并且对称密钥也必须单独管理

反序列化后,何时不是同一个对象

只要将对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,而且只要在同一流中,对象都是同一个。否则,反序列化后的对象地址和原对象地址不同,只是内容相同

如果将一个对象序列化入某文件,那么之后又对这个对象进行修改,然后再把修改的对象重新写入该文件,那么修改无效,文件保存的序列化的对象仍然是最原始的。这是因为,序列化输出过程跟踪了写入流的对象,而试图将同一个对象写入流时,并不会导致该对象被复制,而只是将一个句柄写入流,该句柄指向流中相同对象的第一个对象出现的位置。为了避免这种情况,在后续的 writeObject() 之前调用 out.reset() 方法,这个方法的作用是清除流中保存的写入对象的记录

安装 serialVersionUID 插件即可。

ArrayList实现了java.io.Serializable接口,但是其 elementData 是 transient 的,但是 ArrayList 是通过数组实现的,数组 elementData 用来保存列表中的元素。通过该属性的声明方式知道该数据无法通过序列化持久化。

但是如果实际测试,就会发现,ArrayList 能被完整的序列化,原因是在writeObject 和 readObject方法中进行了序列化的实现。

这样设计的原因是因为 ArrayList 是动态数组,如果数组自动增长长度设为 2000,而实际只放了一个元素,那就会序列化 1999 个 null 元素,为了保证在序列化的时候不会将这么多 null 元素序列化,ArrayList 把元素数组设置为transient,但是,作为一个集合,在序列化过程中还必须保证其中的元素可以被持久化,所以,通过重写 writeObject 和 readObject 方法把其中的元素保留下来,具体做法是:

writeObject方法把elementData数组中的元素遍历到ObjectOutputStream

readObject方法从ObjectInputStream中读出对象并保存赋值到elementData数组

Original: https://www.cnblogs.com/kubixuesheng/p/10350523.html
Author: dashuai的博客
Title: Java对象序列化全面总结

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/538036/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • docker 安装mysql5.7

    拉取镜像 docker pull mysql:5.7 准备数据目录 mkdir -p /mall/docker/mysql/conf mkdir -p /mall/docker/m…

    Java 2023年6月9日
    046
  • 如何生成一个java文档

    如何生成一个java文档 众所周知,一个程序给别人看可能可以看懂,几万行程序就不一定了。在更多的时候,我们并不需要让别人知道我们的程序是怎么写的,只需要告诉他们怎么用的。那么,ap…

    Java 2023年6月9日
    079
  • Java基础-多线程学习目录

    多线程学习目录 Java多线程并发编程一览笔录 什么时候使用CountDownLatch Java并发学习系列-绪论 God, Grant me the SERENITY, to …

    Java 2023年5月29日
    066
  • Springboot限流工具之CurrentLimiting

    1.工具简介 CurrentLimiting:基于令牌桶算法和漏桶算法实现的纳秒级分布式无锁限流插件,完美嵌入SpringBoot、SpringCloud应用,支持接口限流、方法限…

    Java 2023年5月30日
    079
  • c#反射

    待总结 posted @2015-03-19 12:58 zhepama 阅读(134 ) 评论() 编辑 Original: https://www.cnblogs.com/zh…

    Java 2023年5月30日
    084
  • Java常用类(一)

    Java常用类(一) Java常用类(一) – 一、String 类:(不可变的字符序列) 1.1 String:字符串,使用一对 ” ” 引起…

    Java 2023年6月9日
    095
  • 如何下载 blob 地址的视频资源

    如何下载视频资源以blob:http开头的资源 一、问题场景 想下载知乎视频资源,却发现视频链接是这个样子的 blob:https://v.vzuu.com/b6146956-6e…

    Java 2023年6月9日
    0100
  • Java面向对象之构造器

    新手菜鸟看完Java教学视频后总结的关于构造器的理解。 面向对象 构造器 从构造器的作用来理解 new的本质是在调用构造器 当我们new一个新的对象的时候,就已经是调用了一个新的构…

    Java 2023年6月9日
    071
  • 数据库时间格式处理

    使用 DateUtil转换,这个还是比较常用的一种,下面贴代码(可以直接复制使用): /** * 日期工具类,注意导包import和package * StringUtils,Da…

    Java 2023年6月7日
    064
  • 第五章 Mac系统软件-安装Java Web开发环境基本软件

    大家好,这是入手Macbook Pro的第三周了,最近公司启动比较多项目,都需要经过自己去安排,所以会比较忙,抽不出来比较多的时间来更新文档,只能是下班挤一点时间来进行总结。 这个…

    Java 2023年5月29日
    076
  • HTML登录功能

    HTML登录其实和JSP登录一样,很简单只要每次访问首页都发送AJAX请求到接口查看Session中是否有用户数据,有则是登录状态,无则是没有登录,之前把问题想的太复杂了,本想优化…

    Java 2023年6月6日
    080
  • 如何使用Java代码修改数组大小呢?

    数组是Java开发中非常重要的一个数据存储容器, 那可以存储多种类型,基础类型,引用类型,但是它有一个缺点,就是一旦创建后,就不可以修改数组的大小, 那么我们如何动态的扩容数组的大…

    Java 2023年6月15日
    068
  • 实现多线程大的三种方式,超级简单的教程

    一。实现多线程的第一种方式 1.继承Thread类 2.启用这个线程 二。实现多线程的第二种方式 1. 实现Runnable接口 2.启用这个线程 三。实现多线程的第三种方式(注意…

    Java 2023年6月9日
    061
  • git reset 命令删除本地文件怎么恢复

    执行 git reflog命令可以看到曾经执行过的操作,还有版本序号。 执行 git reset –hard HEAD@{【填那个序号】}就可以恢复本地删除的文件了! …

    Java 2023年6月15日
    098
  • Java后端代码规范与优化建议

    1、尽量指定类、方法的final修饰符 带有final修饰符的类是不可派生的。在Java核心API中,有许多应用final的例子,例如java.lang.String,整个类都是f…

    Java 2023年5月29日
    058
  • Kafka 生产者

    一个消息系统说白了无非就是由三部分组成,不同的消息系统只是这三部分的实现不同,或者会在这三部分之外扩充自己的特性。这三部分分别就是:生产者、消费者、消息队列 这篇文章主要介绍的是 …

    Java 2023年6月8日
    0132
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球