是时候放弃 Java 序列化了

news2024/11/28 4:48:05

基本概念

Java 序列化和反序列化三连问:

  • 什么是 Java 序列化和反序列化?
  • 为什么需要 Java 序列化和反序列化?
  • 如何实现 Java 序列化和反序列化?

是什么

一句话就能够说明白什么是 Java 序列化和反序列化?Java 序列化是将 Java 对象转换为字节序列的过程,而 Java 反序列化则是将字节序列恢复为 Java 对象的过程。

  • 序列化:任何需要保存到磁盘或者在网络进行传输的 Java 对象都需要支持序列化,序列化后的字节流保存了 Java 对象的状态及相关的描述信息,反序列化能够根据这些信息“复刻”出一个一模一样的对象。序列化的核心作用就是对象状态的保存。
  • 反序列化:反序列化就是根据磁盘中保存的或者网络上传输的字节流中所保存的对象状态和相关描述信息,通过反序列化重建对象。

所以,从本质上来说,序列化就是将对象的状态和相关描述信息按照一定的格式写入到字节流中,而反序列化则是从字节流中重建这个对象。

为什么

为什么需要 Java 序列化和反序列化呢?有两个原因:

  1. 持久化。即将该对象保存到磁盘中。一般来说我们是不需要持久化 Java 对象的,但是如果遇到特殊情况,我们需要将 Java 对象持久化到磁盘中,以便于我们在重启 JVM 时可以重建这些 Java 对象。所以我们可以通过序列化的方式将 Java 对象转换成字节流,然后将这些字节流保存到磁盘中实现持久化。在我们应用程序重启时,可以读取这些字节流进行反序列化还原 Java 对象。
  2. 网络传输:我们都知道网络上传输的对象是二进制字节流,我们是无法传输一个 Java 对象给一个应用的,所以在传输前我们需要对 Java 对象进行序列化将其转换为字节流。而接收方则根据字节流中所包含的信息重建该 Java 对象。

怎么做?

在 Java 中,如果一个对象要想实现序列化,它有两种方式:

  1. 实现 Serializable 接口
  2. 实现 Externalizable 接口

这两个接口是如何工作的呢?又有什么区别呢?下面我们分别介绍。

Java 如何实现序列化和反序列化

Serializable 接口

Serializable 接口只是一个标记接口,不用实现任何方法。一个对象只要实现了该接口,就意味着该对象是可序列化的。

序列化

Java 对象序列化的步骤如下:

  1. 对象实现 Serializable 接口
  2. 创建一个 ObjectOutputStream 输出流
  3. 调用 ObjectOutputStream 对象的 writeObject() 输出可序列化对象

如下:

@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Person implements Serializable {

    private String name;

    private Integer age;

    private Float height;
}

public class Serializable01 {

    public static void main(String[] args) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person01.txt"));
        Person person01 = new Person("张三",35,175.4F);
        oos.writeObject(person01);
    }
}

用 idea 打开 person01.txt 文件就可以得到如下内容:

从这个文件中我们基本上可以看清楚 Person01 对象的字节流的轮廓。

反序列化

Java 反序列化步骤如下:

  1. 对象实现 Serializable 接口
  2. 创建一个 ObjectInputStream 对象
  3. 调用 ObjectInputStream 对象的 readObject()
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person01.txt"));
Person person011 = (Person01) ois.readObject();
System.out.println("person01.txt 反序列化内容:" + person011.toString());

运行结果

person01.txt 反序列化内容:Person01(name=张三, age=35, height=175.4)

反序列化生成的对象和序列化的对象内容一模一样,完全还原了序列化时的对象。

成员为引用的序列化

上面的例子 Person 的成员变量都是基本类型,如果成员变量为引用类型呢?

我们去掉 Person 类实现的 Serializable 接口,然后定义一个 Women 类。

public class Person {

    private String name;

    private Integer age;

    private Float height;
}

public class Woman implements Serializable {

    private String hairColor;

    private Person person;
}

我们再来序列化 Woman 这类

public class Serializable02 {

    public static void main(String[] args) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("woman.txt"));

        Person person = new Person("李四",30,180F);
        Woman woman = new Woman("黄颜色",person);

        oos.writeObject(woman);
    }
}

执行时,你会发现程序会抛出异常:

java.io.NotSerializableException: com.sike.javacore.serializer.serializable.dto.Person
...

所以,一个可序列化的类,如果它含有引用类型的成员变量,那么这个引用类型也必须是可序列化的。

自定义序列化

有些时候我们并不需要将一个对象的所有属性全部序列化,这个时候我们可以使用 transient 关键字来选择不需要序列化的字段。

transient** 的作用就是用来标识一个成员变量在序列化应该被忽略。**

public class Person_1 implements Serializable {

    private String name;
    
    // 标识为 transient
    private transient Integer age;

    private Float height;
}

将 age 属性标识为 transient。

public class Serializable03 {

    public static void main(String[] args) throws Exception {
        // 先序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person_1.txt"));
        Person_1 person = new Person_1("王五",32,180F);
        oos.writeObject(person);
        System.out.println("原对象:" + person);

        // 再反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person_1.txt"));
        Person_1 person1 = (Person_1) ois.readObject();
        System.out.println("序列化后对象:" + person1);
    }
}

运行结果:

原对象:Person_1(name=王五, age=32, height=180.0)
序列化后对象:Person_1(name=王五, age=null, height=180.0)

从运行结果我们可以看出,用 transient 标识的属性,在进行序列化时会将该字段忽略,然后在反序列化的时候,被 transient 标识的属性会被设置为默认值。

Externalizable 接口

一个类除了实现 Serializable 接口外来实现序列化,还有一种更加灵活的方式来实现序列化:实现 Externalizable 接口

Externalizable 接口是 Serializable 的子类,它提供了 writeExternal()readExternal() 方法让类能够更加灵活地实现序列化。

public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;

    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

一个类如果实现了 Externalizable 接口,即必须要实现 writeExternal()readExternal() 两个方法。在这两个方法里面你可以做自己任何想做的事情。

public class Student implements Externalizable {

    private String name;

    private int age;

    private int grade;

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age - 2);      // 年龄我虚报 2 岁
        // 成绩我不报了
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = (String) in.readObject();
        this.age = in.readInt();
    }
}

public class Serializable04 {

    public static void main(String[] args) throws Exception {
        // 先序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.txt"));
        Student student = new Student("小明",15,55);
        oos.writeObject(student);
        System.out.println("序列化对象内容:" + student);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.txt"));
        Student student1 = (Student) ois.readObject();
        System.out.println("序列化后的内容:" + student1);
    }
}

运行结果:

序列化对象内容:Student(name=小明, age=15, grade=55)
序列化后的内容:Student(name=小明, age=13, grade=0)

根据运行结果我们看到,Externalizable 接口可以实现自定义的序列化和反序列化。

但是使用 Externalizable 接口时要注意,writeExternal() 方法和 readExternal() 的顺序要一致,即 writeExternal() 是按照怎么样的顺序来 write 值的,readExternal() 就必须严格按照这个顺序来 read ,否则会报错。有兴趣的小伙伴可以 name 和 age 的顺序调整下,就知道了。

Serializable 和 Externalizable 对比

serializableExternalizable
系统自动存储 Java 对象必要的信息程序员自己来实现 Java 对象的序列化,灵活度更加高
不需要的属性使用 transient 修饰不需要的属性可以不写入对象
在反序列化的时候不走构造方法反序列化时,先走无参构造方法得到一个空对象,在调用 readExternal() 方法来读取序列化文件中的内容给该空对象赋值

serialVersionUID 版本号

我们先看一个例子。

我们先将 Student 对象序列化到本地磁盘 student.txt 文件中,然后在 Student 类里面增加一个字段,比如 className,用来表示所在的班级,然后再用刚刚已经序列化的 student.txt 来反序列化试图还原 Student 对象,这个时候你会发现运行报错,抛出下面的异常:

Exception in thread "main" java.io.InvalidClassException: com.sike.javacore.serializer.serializable.dto.Student; local class incompatible: stream classdesc serialVersionUID = -1065600830313514941, local class serialVersionUID = 2126309100823681

异常信息说明:序列化前后的 serialVersionUID 不一致。一个是 serialVersionUID = -1065600830313514941,另外一个是 serialVersionUID = 2126309100823681。

为什么两个 serialVersionUID 会不一样呢?因为我们对 Student 类做了变更,即所谓的升级。

在我们实际开发中,我们的 Class 文件不可能一成不变,它是随着项目的升级,Class 文件也会 升级,但是我们不能因为升级了 Class 类就导致之前的序列化对象无法还原了,我们需要做到升级前后的兼容性。怎么保证呢?显示声明 serialVersionUID。

Java 序列化提供了一个 private static final long serialVersionUID = xxxx 的序列化版本号,只要版本号相同,就可以将原来的序列化对象还原。

类的序列化版本号 serialVersionUID 可以随意指定,如果不指定,则 JVM 会根据类信息自己生成一个版本号,但是这样就会无法保证类升级后的序列化了。同时,不指定版本号也不利于 JVM 间的移植,因为可能不同的 JVM 版本计算规则可能就不一样了,这样也会导致无法反序列化。所以,凡是实现 Serializable 接口的类,我们都需要显示声明一个 serialVersionUID 版本号。

缺点

说实在话,现在几乎不会有人使用 Java 原生的序列化了,有如下几个原因使得我们不得不嫌弃他。

无法跨语言

通过 Java 原生 Serializable 接口与 ObjectOutputStream 实现的序列化,只能通过 Java 语言自己的ObjectInputStream 来反序列化,其他语言,如 C、Python、Go 等等都无法对其进行反序列化,这不很坑么?

同时,跨平台支持也不是很好,客户端与服务端如果因为 JDK 的版本不同都有可能导致无法进行反序列化,这个就更加坑了。

序列化字节流太大

Java 序列化它需要将类的描述信息和属性进行序列化,如果不这样做,它根本无法还原,这就会导致序列化字节流变得很大。我们来做一个比较,一个是 Java 原生序列化,一个是通用的二进制编码。

public class UserInfo implements Serializable {
    private static final long serialVersionUID = 1L;

    private Long id;

    private String userName;

    private String nickName;

    public byte[] codeC() {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        byte[] userNameBytes = this.userName.getBytes();
        buffer.putInt(userNameBytes.length);
        buffer.put(userNameBytes);
        byte[] nickNameBytes = this.nickName.getBytes();
        buffer.putInt(nickNameBytes.length);
        buffer.put(nickNameBytes);
        buffer.putLong(this.id);
        buffer.flip();
        byte[] result = new byte[buffer.remaining()];
        buffer.get(result);
        return result;
    }
}

UserInfo 类有一个 codeC() 方法,该方法返回 UserInfo 的字节流。

public class Serializable01 {
    public static void main(String[] args) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person01.txt"));
        Person person01 = new Person("张三",35,175.4F);
        oos.writeObject(person01);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person01.txt"));
        Person person011 = (Person) ois.readObject();
        System.out.println("person01.txt 反序列化内容:" + person011.toString());
    }
}

运行结果:

原生 JDK 序列化生成的字节流大小:246
UserInfo 对象字节流大小:31

有 8 倍的差距,这差距还是有点儿大的。

序列化时间太长

还是上面那个类,我们把上面的程序改下:

public class Serializable05 {

    public static void main(String[] args) throws Exception {
        UserInfo userInfo = new UserInfo(1001L,"zhangshan","张三");

        // 序列化
        long startTime = System.currentTimeMillis();
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(bout);
        out.writeObject(userInfo);
        out.flush();
        out.close();
        System.out.println("原生 JDK 序列化消耗时间:" + (System.currentTimeMillis() - startTime));
        bout.close();

        // 原生字节码
        startTime = System.currentTimeMillis();
        userInfo.codeC();
        System.out.println("UserInfo#codeC 消耗时间:" + (System.currentTimeMillis() - startTime));
    }
}

运行结果:

原生 JDK 序列化消耗时间:9
UserInfo#codeC 消耗时间:1

这差距依然很巨大啊!

所以,Java 原生序列化这么弱,也不能不让我们嫌弃他啊!!!

总结

下面对 Java 序列化做一个总结。

  1. 序列化的目的是为了将 Java 对象的状态持久化存储起来或者在网络上传输。
  2. 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
  3. 如果要序列化的类中包含有引用类型的成员变量,那么该成员变量也需要支持序列化。
  4. 反序列化时必须要有序列化对象的 Class 文件(这里埋坑了)。
  5. 对于 Serializable 接口而言,它只是起到一个标识作用。实现了该接口就意味着该类支持序列化。
    1. 如果我们不想要某个变量被序列化,使用 transient 修饰。
  6. 对于 Externalizable 接口
    1. Externalizable 接口是 Serializable 的子类,它提供了 writeExternal()readExternal() 方法类实现自定义的序列化和反序列化。
    2. writeExternal()readExternal() 两个方法对属性的加工顺序要一致。
  7. 建议所有实现了 Serializable 接口的类都显示申明 serialVersionUID 版本号。

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

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

相关文章

【探索Linux】—— 强大的命令行工具 P.13(文件系统 | 软硬链接 | 动态库和静态库)

阅读导航 引言一、文件系统1. 磁盘文件系统2. 磁盘结构(1)物理结构(2)存储结构 3. stat 命令4. Linux ext2文件系统 二、软硬链接1. 软连接2. 硬链接 三、动态库和静态库1. 动态库(1)动态库文件扩展名&…

计算虚拟化1——CPU虚拟化

目录 vCPU的概念 vCPU和CPU的关系 CPU的Ring级别 CPU虚拟化技术 软件辅助全虚拟化 半虚拟化 硬件辅助虚拟化 计算资源的虚拟化可以分为CPU虚拟化、内存虚拟化、I/O虚拟化三个方面 CPU虚拟化:多个虚拟机共享CPU资源,对虚拟机中的敏感指令进行截获…

【JavaSE】基础笔记 - 类和对象(上)

目录 1、面向对象的初步认知 1.1、什么是面向对象 1.2、面向对象与面向过程 2. 类定义和使用 2.1、简单认识类 2.2、类的定义格式 2.3、自定义类举例说明 2.3.1、定义一个狗类 2.3.2、定义一个学生类 3、类的实例化 3.1、什么是实例化 3.2、类和对象的说明 1、面向…

MySQL性能优化的最佳20条经验

概述 关于数据库的性能,这并不只是DBA才需要担心的事。当我们去设计数据库表结构,对操作数据库时(尤其是查表时的SQL语句),我们都需要注意数据操作的性能。下面讲下MySQL性能优化的一些点。 1. 为查询缓存优化你的查询 大多数的MySQL服务器…

Python基础入门例程47-NP47 牛牛的绩点(条件语句)

最近的博文: Python基础入门例程46-NP46 菜品的价格(条件语句)-CSDN博客 Python基础入门例程45-NP45 禁止重复注册(条件语句)-CSDN博客 Python基础入门例程44-NP44 判断列表是否为空(条件语句&#xff0…

ElasticSearch 实现 全文检索 支持(PDF、TXT、Word、HTML等文件)通过 ingest-attachment 插件实现 文档的检索

一、Attachment 介绍 Attachment 插件是 Elasticsearch 中的一种插件,允许将各种二进制文件(如PDF、Word文档等)以及它们的内容索引到 Elasticsearch 中。插件使用 Apache Tika 库来解析和提取二进制文件的内容。通过使用 Attachment 插件&a…

redis数据库缓存服务器(基础命令)

redis比mysql访问数据快 非关系型数据库以键值对的方式存储数据 作用:加快访问速度,缓解数据库压力 redis最新版本7 特点 丰富的数据结构 list,set,hash等数据结构的存储 支持持久化 支持事务 “一个完整的动作,要么全部执行&#xff0…

数据结构:AVL树讲解(C++)

AVL树 1.AVL树的概念2.平衡因子3.节点的定义4.插入操作5.旋转操作(重点)5.1左单旋5.2右单旋5.3左右双旋5.4右左双旋 6.一些简单的测试接口7.完整代码 1.AVL树的概念 普通二叉搜索树:二叉搜索树 二叉搜索树虽可以缩短查找的效率,但…

D-Link监控账号密码信息泄露

访问漏洞的 url 为 /config/getuser?index0其中泄露了账号密码 使用泄露的账号密码登陆系统 文笔生疏,措辞浅薄,望各位大佬不吝赐教,万分感谢。 免责声明:由于传播或利用此文所提供的信息、技术或方法而造成的任何直接或间接的…

CCF-CSP真题《202309-3 梯度求解》思路+python,c++满分题解

想查看其他题的真题及题解的同学可以前往查看:CCF-CSP真题附题解大全 试题编号:202309-3试题名称:梯度求解时间限制:1.0s内存限制:512.0MB问题描述: 背景 西西艾弗岛运营公司近期在大力推广智能化市政管理系…

文本生成评估指标简单介绍BLEU+ROUGE+Perplexity+Meteor 代码实现

以下指标主要针对两种:机器翻译和文本生成(文章生成),这里的文本生成并非是总结摘要那类文本生成,仅仅是针对生成句子/词的评价。 首先介绍BLEU,ROUGE, 以及BLEU的改进版本METEOR;后半部分介绍P…

volatile-禁重排案例详解

在每一个volatile写操作前面插入一个StoreStore屏障--->StoreStore屏障可以保证在volatile写之前,其前面所有的普通写操作都已经刷新到主内存中。 在每一个volatile写操作后面插入一个StoreLoad屏障--->StoreLoad屏障的作用是避免volatile写与后面可能有的vo…

【Qt之QtXlsx模块】安装及使用

1. 安装Perl,编译QtXlsx源码用 可以通过命令行进行查看是否已安装Perl。 下载及安装传送门:链接: https://blog.csdn.net/MrHHHHHH/article/details/134233707?spm1001.2014.3001.5502 1.1 未安装 命令:perl --version 显示以上是未安装…

测试面试题集-UI自动化测试

1、列举web自动化中常见的元素定位方式? id:根据id来获取元素,返回单个元素,id值一般是唯一的;name:根据元素的name属性定位;tagName:根据元素的标签名定位;className&a…

基于国产仪器的某海上平台SPM振动监测系统

摘要:使用分布式采集仪和云智慧监测系统,实现海上浮式储油轮的单点SPM(水上水下有轴承的钢结构)振动监测,经受住了湿度高、气温变化大等多变的气候环境的考验。 关键词:石油平台,振动监测,环境 某海上浮式…

【vue2高德地图api】04-poi搜索

系列文章目录 文章目录 系列文章目录前言一、高德地图文档入口二、使用步骤1.创建文件以及路由2.编写页面代码3.样式4变量以及方法5.编写查询方法 总结 前言 提示:这里可以添加本文要记录的大概内容: 本篇要实现的功能,看下图 提示&#x…

FinalCutPro 移动项目的时候,遇到失去连接的文件怎么处理

FinalCutPro 移动项目的时候,遇到失去连接的文件怎么处理 有时候,FinalCutPro 项目在移动之后,一些链接到外面的文件会失去连接,文件虽然还在原有位置,但显示成下面这样: 解决方法 1. 点击菜单【文件】…

Java VMTranslator Part II

用Java写一个翻译器,将Java的字节码翻译成汇编语言 目录 程序控制流开发 基本思路 核心代码 实验结果,使用例子进行验证 函数调用 基本思路 核心代码 实验结果,使用例子进行验证 Parser CodeWriter Main 程序控制流开发 基本思路…

Activiti监听器

文章目录 学习链接任务监听器 TaskListener监听器监听的事件监听器委托类DelegateTask任务监听实现方式 — 类class绘制流程图自定义任务监听器SiteReportUserTaskListener 测试 监听实现方式 — 表达式expression绘制流程图自定义 TaskListenerExpression测试spring表达式 执行…