Java学习之序列化

news2025/1/23 2:20:51

1、引言

《手册》第 9 页 “OOP 规约” 部分有一段关于序列化的约定 1:

【强制】当序列化类新增属性时,请不要修改 serialVersionUID 字段,以避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。
说明:注意 serialVersionUID 值不一致会抛出序列化运行时异常。

我们应该思考下面几个问题:

序列化和反序列化到底是什么?
它的主要使用场景有哪些?
Java 序列化常见的方案有哪些?
各种常见序列化方案的区别有哪些?
实际的业务开发中有哪些坑点?
接下来将从这几个角度去研究这个问题。

2、序列化和反序列化是什么?为什么需要它?

序列化是将内存中的对象信息转化成可以存储或者传输的数据到临时或永久存储的过程。而反序列化正好相反,是从临时或永久存储中读取序列化的数据并转化成内存对象的过程。

file

那么为什么需要序列化和反序列化呢?

我们都知道,文本文件,图片、视频和安装包等文件底层都被转化为二进制字节流来传输的,对方得文件就需要对文件进行解析,因此就需要有能够根据不同的文件类型来解码出文件的内容的程序。

如果要实现 Java 远程方法调用,就需要将调用结果通过网路传输给调用方,如果调用方和服务提供方不在一台机器上就很难共享内存,就需要将 Java 对象进行传输。而想要将 Java 中的对象进行网络传输或存储到文件中,就需要将对象转化为二进制字节流,这就是所谓的序列化。存储或传输之后必然就需要将二进制流读取并解析成 Java 对象,这就是所谓的反序列化。

序列化的主要目的是:方便存储到文件系统、数据库系统或网络传输等。

实际开发中常用到序列化和反序列化的场景有:

  • 远程方法调用(RPC)的框架里会用到序列化。
  • 将对象存储到文件中时,需要用到序列化。
  • 将对象存储到缓存数据库(如 Redis)时需要用到序列化。
  • 通过序列化和反序列化的方式实现对象的深拷贝。

3、常见的序列化方式

常见的序列化方式包括 Java 原生序列化、Hessian 序列化、Kryo 序列化、JSON 序列化等。

3.1 Java 原生序列化

正如前面章节讲到的,对于 JDK 中有的类,最好的学习方式之一就是直接看其源码。

Serializable 的源码非常简单,只有声明,没有属性和方法:

public interface Serializable {
}

先思考一个问题:如果一个类序列化到文件之后,类的结构发生变化还能否保证正确地反序列化呢?
答案显然是不确定的。

所以每个序列化类都有一个叫 serialVersionUID 的版本号,反序列化时会校验待反射的类的序列化版本号和加载的序列化字节流中的版本号是否一致,如果序列化号不一致则会抛出 InvalidClassException 异常。

强烈推荐每个序列化类都手动指定其 serialVersionUID,如果不手动指定,那么编译器会动态生成默认的序列化号,因为这个默认的序列化号和类的特征以及编译器的实现都有关系,很容易在反序列化时抛出 InvalidClassException 异常。建议将这个序列化版本号声明为私有,以避免运行时被修改。

实现序列化接口的类可以提供自定义的函数修改默认的序列化和反序列化行为。

//自定义序列化方法:
private void writeObject(ObjectOutputStream out) throws IOException;

//自定义反序列化方法
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;

通过自定义这两个函数,可以实现序列化和反序列化不可序列化的属性,也可以对序列化的数据进行数据的加密和解密处理。

3.2 Hessian 序列化

Hessian 是一个动态类型,二进制序列化,也是一个基于对象传输的网络协议。Hessian 是一种跨语言的序列化方案,序列化后的字节数更少,效率更高。Hessian 序列化会把复杂对象的属性映射到 Map 中再进行序列化。

3.3 Kryo 序列化

Kryo 是一个快速高效的 Java 序列化和克隆工具。Kryo 的目标是快速、字节少和易用。Kryo 还可以自动进行深拷贝或者浅拷贝。Kryo 的拷贝是对象到对象的拷贝而不是对象到字节,再从字节到对象的恢复。Kryo 为了保证序列化的高效率,会提前加载需要的类,这会带一些消耗,但是这是序列化后文件较小且反序列化非常快的重要原因。

3.4 JSON 序列化

JSON (JavaScript Object Notation) 是一种轻量级的数据交换方式。JSON 序列化是基于 JSON 这种结构来实现的。JSON 序列化将对象转化成 JSON 字符串,JSON 反序列化则是将 JSON 字符串转回对象的过程。常用的 JSON 序列化和反序列化的库有 Jackson、GSON、Fastjson 等。

4、Java 常见的序列化方案对比

我们想要对比各种序列化方案的优劣无外乎两点,一点是查资料,一点是自己写代码验证。

4.1 Java 原生序列化

Java 序列化的优点是:对对象的结构描述清晰,反序列化更安全。主要缺点是:效率低,序列化后的二进制流较大。

4.2 Hessian 序列化

Hession 序列化二进制流较 Java 序列化更小,且序列化和反序列化耗时更短。但是父类和子类有相同类型属性时,由于先序列化子类再序列化父类,因此反序列化时子类的同名属性会被父类的值覆盖掉,开发时要特别注意这种情况。
Hession2.0 序列化二进制流大小是 Java 序列化的 50%,序列化耗时是 Java 序列化的 30%,反序列化的耗时是 Java 序列化的 20%。

4.3 Kryo 序列化

Kryo 优点是:速度快、序列化后二进制流体积小、反序列化超快。但是缺点是:跨语言支持复杂。注册模式序列化更快,但是编程更加复杂。

4.4 JSON 序列化

JSON 序列化的优势在于可读性更强。主要缺点是:没有携带类型信息,只有提供了准确的类型信息才能准确地进行反序列化,这点也特别容易引发线上问题。

下面给出使用 Gson 框架模拟 JSON 序列化时遇到的反序列化问题的示例代码:

/**
 * 验证GSON序列化类型错误
 */
@Test
public void testGSON() {
    Map<String, Object> map = new HashMap<>();
    final String name = "name";
    final String id = "id";
    map.put(name, "张三");
    map.put(id, 20L);

    String jsonString = GSONSerialUtil.getJsonString(map);
    Map<String, Object> mapGSON = GSONSerialUtil.parseJson(jsonString, Map.class);
    // 正确
    Assert.assertEquals(map.get(name), mapGSON.get(name));
    // 不等  map.get(id)为Long类型 mapGSON.get(id)为Double类型
    Assert.assertNotEquals(map.get(id).getClass(), mapGSON.get(id).getClass());
    Assert.assertNotEquals(map.get(id), mapGSON.get(id));
}

下面给出使用 fastjson 模拟 JSON 反序列化问题的示例代码:

/**
 * 验证FatJson序列化类型错误
 */
@Test
public void testFastJson() {
    Map<String, Object> map = new HashMap<>();
    final String name = "name";
    final String id = "id";
    map.put(name, "张三");
    map.put(id, 20L);

    String fastJsonString = FastJsonUtil.getJsonString(map);
    Map<String, Object> mapFastJson = FastJsonUtil.parseJson(fastJsonString, Map.class);

    // 正确
    Assert.assertEquals(map.get(name), mapFastJson.get(name));
    // 错误  map.get(id)为Long类型 mapFastJson.get(id)为Integer类型
    Assert.assertNotEquals(map.get(id).getClass(), mapFastJson.get(id).getClass());
    Assert.assertNotEquals(map.get(id), mapFastJson.get(id));
}

大家还可以通过单元测试构造大量复杂对象对比各种序列化方式或框架的效率。

如定义下列测试类为 User,包括以下多种类型的属性:

@Data
public class User implements Serializable {
    private Long id;
    private String name;
    private Integer age;
    private Boolean sex;
    private String nickName;
    private Date birthDay;
    private Double salary;
}

4.5 各种常见的序列化性能排序

实验的版本:kryo-shaded 使用 4.0.2 版本,gson 使用 2.8.5 版本,hessian 用 4.0.62 版本。

实验的数据:构造 50 万 User 对象运行多次。

大致得出一个结论:

从二进制流大小来讲:JSON 序列化 > Java 序列化 > Hessian2 序列化 > Kryo 序列化 > Kryo 序列化注册模式;
从序列化耗时而言来讲:GSON 序列化 > Java 序列化 > Kryo 序列化 > Hessian2 序列化 > Kryo 序列化注册模式;
从反序列化耗时而言来讲:GSON 序列化 > Java 序列化 > Hessian2 序列化 > Kryo 序列化注册模式 > Kryo 序列化;
从总耗时而言:Kryo 序列化注册模式耗时最短。
注:由于所用的序列化框架版本不同,对象的复杂程度不同,环境和计算机性能差异等原因结果可能会有出入。

5、 序列化引发的一个血案

接下来我们看下面的一个案例:

前端调用服务 A,服务 A 调用服务 B,服务 B 首次接到请求会查 DB,然后缓存到 Redis(缓存 1 个小时)。服务 A 根据服务 B 返回的数据后执行一些处理逻辑,处理后形成新的对象存到 Redis(缓存 2 个小时)。

服务 A 通过 Dubbo 来调用服务 B,A 和 B 之间数据通过 Map<String,Object> 类型传输,服务 B 使用 Fastjson 来实现 JSON 的序列化和反序列化。

服务 B 的接口返回的 Map 值中存在一个 Long 类型的 id 字段,服务 A 获取到 Map ,取出 id 字段并强转为 Long 类型使用。

执行的流程如下:

file

通过分析我们发现,服务 A 和服务 B 的 RPC 调用使用 Java 序列化,因此类型信息不会丢失。

但是由于服务 B 采用 JSON 序列化进行缓存,第一次访问没啥问题,其执行流程如下:

file

如果服务 A 开启了缓存,服务 A 在第一次请求服务 B 后,缓存了运算结果,且服务 A 缓存时间比服务 B 长,因此不会出现错误。

file

如果服务 A 不开启缓存,服务 A 会请求服务 B ,由于首次请求时,服务 B 已经缓存了数据,服务 B 从 Redis(B)中反序列化得到 Map。流程如下图所示:

file

然而问题来了: 服务 A 从 Map 取出此 Id 字段,强转为 Long 时会出现类型转换异常。

最后定位到原因是 Json 反序列化 Map 时如果原始值小于 Int 最大值,反序列化后原本为 Long 类型的字段,变为了 Integer 类型,服务 B 的同学紧急修复。

服务 A 开启缓存时, 虽然采用了 JSON 序列化存入缓存,但是采用 DTO 对象而不是 Map 来存放属性,所以 JSON 反序列化没有问题。

因此大家使用二方或者三方服务时,当对方返回的是 Map<String,Object> 类型的数据时要特别注意这个问题。

作为服务提供方,可以采用 JDK 或者 Hessian 等序列化方式;

作为服务的使用方,我们不要从 Map 中一个字段一个字段获取和转换,可以使用 JSON 库直接将 Map 映射成所需的对象,这样做不仅代码更简洁还可以避免强转失败。

代码示例:

@Test
public void testFastJsonObject() {
    Map<String, Object> map = new HashMap<>();
    final String name = "name";
    final String id = "id";
    map.put(name, "张三");
    map.put(id, 20L);

    String fastJsonString = FastJsonUtil.getJsonString(map);
    // 模拟拿到服务B的数据
    Map<String, Object> mapFastJson = FastJsonUtil.parseJson(fastJsonString,map.getClass());
    // 转成强类型属性的对象而不是使用map 单个取值
    User user = new JSONObject(mapFastJson).toJavaObject(User.class);
    // 正确
    Assert.assertEquals(map.get(name), user.getName());
    // 正确
    Assert.assertEquals(map.get(id), user.getId());
}

6、课后题

给出一个 PersonTransit 类,一个 Address 类,假设 Address 是其它 jar 包中的类,没实现序列化接口。请使用今天讲述的自定义的函数 writeObject 和 readObject 函数实现 PersonTransit 对象的序列化,要求反序列化后 address 的值正常。

@Data
public class PersonTransit implements Serializable {

    private Long id;
    private String name;
    private Boolean male;
    private List<PersonTransit> friends;
    private Address address;
}

@Data
@AllArgsConstructor
public class Address {
    private String detail;
  }

一、序列化主要有两个困难:

1 transient 关键字,序列化时默认不序列化该字段。(新加的,增加难度)

2 假设 Address 是第三方 jar 包中的类,不允许修改实现序列化接口。

二、分析

我们通过专栏的介绍还有序列化接口java.io.Serializable的注释可知,可以自定义序列化方法和反序列化方法:

private void writeObject(java.io.ObjectOutputStream out)
    throws IOException
 private void readObject(java.io.ObjectInputStream in)
     throws IOException, ClassNotFoundException;

实现序列化和反序列化不可序列化的属性,也可以对序列化的数据进行数据的加密和解密处理。

三、参考代码

@Data
public class PersonTransit implements Serializable {
 
    private Long id;
 
    private String name;
 
    private Boolean male;
 
    private List<PersonTransit> friends;
 
    private transient Address address;
 
    /**
     * 自定义序列化写方法
     */
    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();
        oos.writeObject(address.getDetail());
    }
 
    /**
     * 自定义反序列化读方法
     */
    private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
        ois.defaultReadObject();
        this.setAddress(new Address( (String) ois.readObject()));
    }
}

单元测试

@Test
public void testJDKSerialOverwrite() throws IOException, ClassNotFoundException {
    PersonTransit person = new PersonTransit();
    person.setId(1L);
    person.setName("张三");
    person.setMale(true);
    person.setFriends(new ArrayList<>());
 
    Address address = new Address();
    address.setDetail("某某小区xxx栋yy号");
    person.setAddress(address);
 
    // 序列化
    JdkSerialUtil.writeObject(file, person);
    // 反序列化
    PersonTransit personTransit = JdkSerialUtil.readObject(file);
    // 判断是否相等
    Assert.assertEquals(personTransit.getName(), person.getName());
    Assert.assertEquals(personTransit.getAddress().getDetail(), person.getAddress().getDetail());
}

用到的工具类:

public class JdkSerialUtil {
 
    public static <T> void writeObject(File file, T data) throws IOException {
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));) {
            objectOutputStream.writeObject(data);
            objectOutputStream.flush();
        }
    }
 
    public static <T> void writeObject(ByteArrayOutputStream outputStream, T data) throws IOException {
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);) {
            objectOutputStream.writeObject(data);
            objectOutputStream.flush();
        }
    }
 
    public static <T> T readObject(File file) throws IOException, ClassNotFoundException {
        FileInputStream fin = new FileInputStream(file);
        ObjectInputStream objectInputStream = new ObjectInputStream(fin);
        return (T) objectInputStream.readObject();
    }
 
    public static <T> T readObject(ByteArrayInputStream inputStream) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        return (T) objectInputStream.readObject();
    }
}

通过单元测试验证了我们编写代码的正确性。

7.思考题

为什么我们在前端向后端发送请求时,请求对象没有序列化id也可以正常使用。

因为前端向后端发送请求,实际上是一个反序列化的过程,反序列化不需要序列化id,是json格式转换为对象。如果一个对象在同一台服务器上,那么他们可以共享内存,并不需要序列化。序列一般存在网络传输和io传输的情况下

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/966093.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Sitecore站点更新License

一、简介 Sitecore 是一个基于ASP.NET 技术的 CMS 系统&#xff0c;它不仅具有传统 Web CMS 的所有功能&#xff0c;还集成了 Marketing 营销&#xff08;当然&#xff0c;这个功能价格不菲&#xff09;的功能&#xff0c;可以提供一个一站式的在线营销解决方案。对于 .NET 程…

Google Earth Engine 的缺点和限制

随着 Google Earth Engine 在地球科学和数据计算领域越来越流行&#xff0c;网上有很多介绍Google Earth Engine 的文章及 Google Earth Engine的追随者。Google Earth Engine确实是一款伟大的产品&#xff0c;我们应该为其点赞。但由于已经有太多人在热捧了&#xff0c;我这里…

在termux下安装pip

termux的包安装命令是pkg或者apt&#xff0c;在termux下安装python包&#xff0c;一般直接pip。 (本笔记适合初初接触termux或者太久没碰termux而遗忘的 coder 翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖免费“圣…

C++ 浅拷贝和深拷贝

目录 1. 浅拷贝 2. 深拷贝 1. 浅拷贝 浅拷贝只是拷贝一个指针&#xff0c;并没有新开辟一个地址&#xff0c;拷贝的指针和原来的指针指向同一块地址&#xff0c;如果原来的指针所指向的资源释放了&#xff0c;那么再释放浅拷贝的指针的资源就会出现错误 对一个已知对象进行拷贝…

在VSCode上画UML的三个插件

2023年9月2日&#xff0c;周六晚上 因为写代理模式的博客时需要画UML&#xff0c;所以就在网上找了半天&#xff0c; 最后觉得VSCode上的这三个插件比较好用 目录 三个画UML的VSCode插件PlantUMLDraw.io IntegrationUMLet我个人推荐使用PlantUML 三个画UML的VSCode插件 Pla…

实战-支付漏洞

免责声明 本文发布的工具和脚本&#xff0c;仅用作测试和学习研究&#xff0c;禁止用于商业用途&#xff0c;不能保证其合法性&#xff0c;准确性&#xff0c;完整性和有效性&#xff0c;请根据情况自行判断。如果任何单位或个人认为该项目的脚本可能涉嫌侵犯其权利&#xff0c…

A 股个股资金流 API 数据接口

A 股个股资金流 API 数据接口 全量股票资金流数据&#xff0c;全量A股数据&#xff0c;最长30日历史数据 1. 产品功能 支持所有A股资金流数据查询&#xff1b;每日定时更新数据&#xff1b;支持多达 30 日历史数据查询&#xff1b;超高的查询效率&#xff0c;数据秒级返回&am…

springboot 与 Redis整合

SpringBoot 操作数据&#xff1a;Spring-data jpa jdbc mongodb redis! SpringData 也是和SpringBoot 齐名的项目&#xff01; 说明&#xff1a;在SpringBoot2.X 之后&#xff0c;原来使用的jedis被替换成了lettuce jedis&#xff1a; 采用的直连&#xff0c;多个线程操作的话&…

【Unity3D】UI Toolkit简介

1 前言 UI Toolkit 是一种基于 Web 技术的 GUI 框架&#xff0c;是为了解决 UGUI 效率问题而设计的新一代 UI 系统&#xff08;UGUI 的介绍详见→UGUI概述&#xff09;。与 UGUI 不同&#xff0c;UI Toolkit 没有采用 GameObject 的方式&#xff0c;而是参考了 Web 技术的 XML …

深度学习应用-WeNet语音识别实战01

概括 本文对WeNet声音识别网络的Python API上介绍的Non-Streaming Usage和 Streaming-Usage分别做了测试&#xff0c;两者本质相同。API对应采样的声音帧率、声道都做了限制。效果还可以&#xff0c;但是部分吐字不清晰、有歧义的地方仍然不能识别清晰。 项目地址&#xff1a; …

内部类总结

内部类 1、内部类介绍&#xff1a; 外 2、成员内部类&#xff1a; 3、静态内部类 4、局部内部类&#xff1a; 5、匿名内部类&#xff1a;

说说TIME_WAIT和CLOSE_WAIT区别

分析&回答 TCP协议规定&#xff0c;对于已经建立的连接&#xff0c;网络双方要进行四次握手才能成功断开连接&#xff0c;如果缺少了其中某个步骤&#xff0c;将会使连接处于假死状态&#xff0c;连接本身占用的资源不会被释放。网络服务器程序要同时管理大量连接&#xf…

使用 Hue 玩转 Amazon EMR(SparkSQL, Phoenix) 和 Amazon Redshift

现状 Apache Hue 是一个基于 Web 的交互式 SQL 助手&#xff0c;通过它可以帮助大数据从业人员&#xff08;数仓工程师&#xff0c;数据分析师等&#xff09;与数据仓库进行 SQL 交互。在 Amazon EMR 集群启动时&#xff0c;通过勾选 Hue 进行安装。在 Hue 启用以后&#xff0…

Scratch 画画的技巧

前言 美术是一种艺术&#xff0c;且不局限于纸张&#xff0c;就像电脑绘图也属于美术。我至今已有三年多的画龄&#xff0c;经验丰富&#xff0c;尤其擅长在scratch造型编辑器上画矢量图。今天给大家分享一些实用的技巧。 1.讲解 用橡皮工具给一个圆擦出“橡皮洞” 橡皮工具&a…

2511. 最多可以摧毁的敌人城堡数目

文章目录 Tag题目来源题目解读解题思路复杂度分析写在最后 Tag 【数组】 题目来源 2511. 最多可以摧毁的敌人城堡数目 题目解读 在数组 forts 中&#xff0c;forts[i] 有三种数值&#xff1a; -1&#xff1a; 表示第 i 个位置没有城堡&#xff0c;是空地&#xff1b;0&…

【计算机知识】Base64 编码说明

一、理论 Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法&#xff0c;由于 2^664&#xff0c;所以每 6 个比特为一个单元&#xff0c;对应某个可打印字符。 Base64 常用于在通常处理文本数据的场合&#xff0c;表示、传输、存储一些二进制数据&#xff0c;包括…

程序员自由创业周记#2:前期准备

感恩 上次公开了创业的决定后&#xff0c;得到了很多亲朋好友和陌生朋友的鼓励或支持&#xff0c;以不同的形式&#xff0c;感动之情溢于言表。这些都会记在心里&#xff0c;大恩不言谢~ 创业方向 笔者是一名资质平平的iOS开发程序猿&#xff0c;创业项目也就是开发App卖&am…

图的深度优先与广度优先遍历

上篇博客介绍了图的概念与图的存储(邻接矩阵、邻接表)&#xff1a; 接下来就是介绍图的遍历。 图的遍历 给定一个图G和其中任意一个顶点v0&#xff0c;从v0出发&#xff0c;沿着途中各边访问图中的所有顶点&#xff0c;且每个顶点仅被遍历一次。"遍历"即对结点进行…

读SQL学习指南(第3版)笔记09_条件逻辑与事务

1. 条件逻辑 1.1. SQL逻辑根据特定列或表达式转向不同的分支来处理 1.2. 在程序执行时从多个路径中选取一个路径的能力 1.3. case表达式 1.3.1. 所有的主流数据库服务器都提供了旨在模拟大多数编程语言中if-then-else 语句的内建函数 1.3.1.1. Oracle的decode()函数 1.3.…

T113-S3-ov5640摄像头调试

目录 前言 一、ov5640模组介绍 1. 图像传感器特性 2. 接口和控制 3. 图像处理能力 4. 应用领域 二、原理图连接 三、设备树配置 四、驱动配置 五、ov5640使用 六、异常记录 总结 前言 摄像头模块是嵌入式系统中常见的外设&#xff0c;用于捕获图像和视频。在本篇文章…