说一说序列化与反序列化中存在的问题

本文涉及的产品
可观测可视化 Grafana 版,10个用户账号 1个月
应用实时监控服务-应用监控,每月50GB免费额度
MSE Nacos 企业版免费试用,1600元额度,限量50份
简介: 本文详细解析了Java中的序列化机制,包括序列化的概念、实现方式及应用场景。通过Student类的实例演示了对象的序列化与反序列化过程,并分析了`Serializable`接口的作用以及`serialVersionUID`的重要意义。此外,文章还探讨了如何通过自定义`readObject()`方法增强序列化的安全性,以及解决可序列化单例模式中可能产生的多实例问题。最后提供了代码示例和运行结果,帮助读者深入理解序列化的原理与实践技巧。

 目录

序列化是做什么的

对象如何序列化?

1、Student类定义

2、序列化

3、反序列化

4、运行结果

Serializable接口有何用?

serialVersionUID号有何用?

约束性加持

单例模式增强


序列化是做什么的

序列化的原本意图是希望对一个Java对象作一下“变换”,变成字节序列,这样一来方便持久化存储到磁盘,避免程序运行结束后对象就从内存里消失,另外变换成字节序列也更便于网络运输和传播,所以概念上很好理解:

  • 序列化:把Java对象转换为字节序列。
  • 反序列化:把字节序列恢复为原先的Java对象。

image.gif

而且序列化机制从某种意义上来说也弥补了平台化的一些差异,毕竟转换后的字节流可以在其他平台上进行反序列化来恢复对象。

对象如何序列化?

然而Java目前并没有一个关键字可以直接去定义一个所谓的“可持久化”对象。

对象的持久化和反持久化需要靠程序员在代码里手动显式地进行序列化和反序列化还原的动作。

举个例子,假如要对Student类对象序列化到一个名为student.txt的文本文件中,然后再通过文本文件反序列化成Student类对象:

image.gif

1、Student类定义

public class Student implements Serializable {
    private String name;
    private Integer age;
    private Integer score;
    @Override
    public String toString() {
        return "Student:" + 'n' +
        "name = " + this.name + 'n' +
        "age = " + this.age + 'n' +
        "score = " + this.score + 'n'
        ;
    }
    // ... 其他省略 ...
}

image.gif

2、序列化

public static void serialize(  ) throws IOException {
    Student student = new Student();
    student.setName("CodeSheep");
    student.setAge( 18 );
    student.setScore( 1000 );
    ObjectOutputStream objectOutputStream = 
        new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
    objectOutputStream.writeObject( student );
    objectOutputStream.close();
    System.out.println("序列化成功!已经生成student.txt文件");
    System.out.println("==============================================");
}

image.gif

3、反序列化

public static void deserialize() throws IOException, ClassNotFoundException {
    ObjectInputStream objectInputStream = 
        new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
    Student student = (Student) objectInputStream.readObject();
    objectInputStream.close();
    System.out.println("反序列化结果为:");
    System.out.println( student );
}

image.gif

4、运行结果

控制台打印:

序列化成功!已经生成student.txt文件
==============================================
反序列化结果为:
Student:
name = CodeSheep
age = 18
score = 1000

image.gif

Serializable接口有何用?

上面在定义Student类时,实现了一个Serializable接口,然而当点进Serializable接口内部查看,发现它竟然是一个空接口,并没有包含任何方法!

image.gif

试想,如果上面在定义Student类时忘了加implements Serializable时会发生什么呢?

实验结果是:此时的程序运行会报错,并抛出NotSerializableException异常:

image.gif

按照错误提示,由源码一直跟到ObjectOutputStreamwriteObject0()方法底层一看,才恍然大悟:

image.gif

如果一个对象既不是字符串、数组、枚举,而且也没有实现Serializable接口的话,在序列化时就会抛出NotSerializableException异常!

原来Serializable接口也仅仅只是做一个标记用!!!

它告诉代码只要是实现了Serializable接口的类都是可以被序列化的!然而真正的序列化动作不需要靠它完成。

serialVersionUID号有何用?

一定经常看到有些类中定义了如下代码行,即定义了一个名为serialVersionUID的字段:

private static final long serialVersionUID = -4392658638228508589L;

image.gif

知道这句声明的含义吗?为什么要搞一个名为serialVersionUID的序列号?

继续来做一个简单实验,还拿上面的Student类为例,并没有人为在里面显式地声明一个serialVersionUID字段。

首先还是调用上面的serialize()方法,将一个Student对象序列化到本地磁盘上的student.txt文件:

public static void serialize() throws IOException {
    Student student = new Student();
    student.setName("CodeSheep");
    student.setAge( 18 );
    student.setScore( 100 );
    ObjectOutputStream objectOutputStream = 
        new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
    objectOutputStream.writeObject( student );
    objectOutputStream.close();
}

image.gif

接下来在Student类里面动点手脚,比如在里面再增加一个名为studentID的字段,表示学生学号:

image.gif

这时候,拿刚才已经序列化到本地的student.txt文件,还用如下代码进行反序列化,试图还原出刚才那个Student对象:

public static void deserialize() throws IOException, ClassNotFoundException {
    ObjectInputStream objectInputStream = 
        new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
    Student student = (Student) objectInputStream.readObject();
    objectInputStream.close();
    System.out.println("反序列化结果为:");
    System.out.println( student );
}

image.gif

运行发现报错了,并且抛出了InvalidClassException异常:

image.gif

这地方提示的信息非常明确了:序列化前后的serialVersionUID号码不兼容!

从这地方最起码可以得出两个重要信息:

  • 1、serialVersionUID是序列化前后的唯一标识符
  • 2、默认如果没有人为显式定义过serialVersionUID,那编译器会为它自动声明一个!

第1个问题: serialVersionUID序列化ID,可以看成是序列化和反序列化过程中的“暗号”,在反序列化时,JVM会把字节流中的序列号ID和被序列化类中的序列号ID做比对,只有两者一致,才能重新反序列化,否则就会报异常来终止反序列化的过程。

第2个问题: 如果在定义一个可序列化的类时,没有人为显式地给它定义一个serialVersionUID的话,则Java运行时环境会根据该类的各方面信息自动地为它生成一个默认的serialVersionUID,一旦像上面一样更改了类的结构或者信息,则类的serialVersionUID也会跟着变化!

所以,为了serialVersionUID的确定性,写代码时还是建议,凡是implements Serializable的类,都最好人为显式地为它声明一个serialVersionUID明确值!

当然,如果不想手动赋值,也可以借助IDE的自动添加功能,比如使用的IntelliJ IDEA,按alt + enter就可以为类自动生成和添加serialVersionUID字段,十分方便:

image.gif

两种特殊情况

  • 1、凡是被static修饰的字段是不会被序列化的
  • 2、凡是被transient修饰符修饰的字段也是不会被序列化的

对于第一点,因为序列化保存的是对象的状态而非类的状态,所以会忽略static静态域也是理所应当的。

对于第二点,就需要了解一下transient修饰符的作用了。

如果在序列化某个类的对象时,就是不希望某个字段被序列化(比如这个字段存放的是隐私值,如:密码等),那这时就可以用transient修饰符来修饰该字段。

比如在之前定义的Student类中,加入一个密码字段,但是不希望序列化到txt文本,则可以:

image.gif

这样在序列化Student类对象时,password字段会设置为默认值null,这一点可以从反序列化所得到的结果来看出:

image.gif

序列化的受控和加强

约束性加持

从上面的过程可以看出,序列化和反序列化的过程其实是有漏洞的,因为从序列化到反序列化是有中间过程的,如果被别人拿到了中间字节流,然后加以伪造或者篡改,那反序列化出来的对象就会有一定风险了。

毕竟反序列化也相当于一种 “隐式的”对象构造 ,因此希望在反序列化时,进行受控的对象反序列化动作。

那怎么个受控法呢?

答案就是: 自行编写readObject()函数,用于对象的反序列化构造,从而提供约束性。

既然自行编写readObject()函数,那就可以做很多可控的事情:比如各种判断工作。

还以上面的Student类为例,一般来说学生的成绩应该在0 ~ 100之间,为了防止学生的考试成绩在反序列化时被别人篡改成一个奇葩值,可以自行编写readObject()函数用于反序列化的控制:

private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException {
    // 调用默认的反序列化函数
    objectInputStream.defaultReadObject();
    // 手工检查反序列化后学生成绩的有效性,若发现有问题,即终止操作!
    if( 0 > score || 100 < score ) {
        throw new IllegalArgumentException("学生分数只能在0到100之间!");
    }
}

image.gif

比如故意将学生的分数改为101,此时反序列化立马终止并且报错:

image.gif

对于上面的代码,有些小伙伴可能会好奇,为什么自定义的privatereadObject()方法可以被自动调用,这就需要跟一下底层源码来一探究竟了,跟到了ObjectStreamClass类的最底层,看到这里一定恍然大悟:

image.gif

又是反射机制在起作用!是的,在Java里,果然万物皆可“反射”(滑稽),即使是类中定义的private私有方法,也能被抠出来执行了,简直引起舒适了。

单例模式增强

一个容易被忽略的问题是:可序列化的单例类有可能并不单例!

举个代码小例子就清楚了。

比如这里先用java写一个常见的「静态内部类」方式的单例模式实现:

public class Singleton implements Serializable {
    private static final long serialVersionUID = -1576643344804979563L;
    private Singleton() {
    }
    private static class SingletonHolder {
        private static final Singleton singleton = new Singleton();
    }
    public static synchronized Singleton getSingleton() {
        return SingletonHolder.singleton;
    }
}

image.gif

然后写一个验证主函数:

public class Test2 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream objectOutputStream =
                new ObjectOutputStream(
                    new FileOutputStream( new File("singleton.txt") )
                );
        // 将单例对象先序列化到文本文件singleton.txt中
        objectOutputStream.writeObject( Singleton.getSingleton() );
        objectOutputStream.close();
        ObjectInputStream objectInputStream =
                new ObjectInputStream(
                    new FileInputStream( new File("singleton.txt") )
                );
        // 将文本文件singleton.txt中的对象反序列化为singleton1
        Singleton singleton1 = (Singleton) objectInputStream.readObject();
        objectInputStream.close();
        Singleton singleton2 = Singleton.getSingleton();
        // 运行结果竟打印 false !
        System.out.println( singleton1 == singleton2 );
    }
}

image.gif

运行后发现:反序列化后的单例对象和原单例对象并不相等了,这无疑没有达到目标。

解决办法是:在单例类中手写readResolve()函数,直接返回单例对象,来规避之:

private Object readResolve() {
    return SingletonHolder.singleton;
}


image.gif

这样一来,当反序列化从流中读取对象时,readResolve()会被调用,用其中返回的对象替代反序列化新建的对象。

如果小假的内容对你有帮助,请点赞评论收藏。创作不易,大家的支持就是我坚持下去的动力!

image.gif

相关文章
|
2月前
|
API
微服务引擎 MSE 及 API 网关 2025 年 4 月产品动态
微服务引擎 MSE 及 API 网关 2025 年 4 月产品动态
311 43
|
11月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
2月前
|
设计模式 算法 Java
设计模式觉醒系列(04)策略模式|简单工厂模式的升级版
本文介绍了简单工厂模式与策略模式的概念及其融合实践。简单工厂模式用于对象创建,通过隐藏实现细节简化代码;策略模式关注行为封装与切换,支持动态替换算法,增强灵活性。两者结合形成“策略工厂”,既简化对象创建又保持低耦合。文章通过支付案例演示了模式的应用,并强调实际开发中应根据需求选择合适的设计模式,避免生搬硬套。最后推荐了JVM调优、并发编程等技术专题,助力开发者提升技能。
|
2月前
|
数据采集 存储 NoSQL
基于Scrapy-Redis的分布式景点数据爬取与热力图生成
基于Scrapy-Redis的分布式景点数据爬取与热力图生成
223 67
|
1月前
|
Java 数据库连接 API
2025 更新必看:Java 编程基础入门级超级完整版指南
本教程为2025更新版Java编程基础入门指南,涵盖开发环境搭建(SDKMAN!管理JDK、VS Code配置)、Java 17+新特性(文本块、Switch表达式增强、Record类)、面向对象编程(接口默认方法、抽象类与模板方法)、集合框架深度应用(Stream API高级操作、并发集合)、模式匹配与密封类等。还包括学生成绩管理系统实战项目,涉及Maven构建、Lombok简化代码、JDBC数据库操作及JavaFX界面开发。同时提供JUnit测试、日志框架使用技巧及进阶学习资源推荐,助你掌握Java核心技术并迈向高级开发。
145 5
|
2月前
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
|
7月前
|
NoSQL Java 关系型数据库
Liunx部署java项目Tomcat、Redis、Mysql教程
本文详细介绍了如何在 Linux 服务器上安装和配置 Tomcat、MySQL 和 Redis,并部署 Java 项目。通过这些步骤,您可以搭建一个高效稳定的 Java 应用运行环境。希望本文能为您在实际操作中提供有价值的参考。
433 26
|
1月前
|
存储 安全 Java
2025 年一线互联网大厂最新高质量 Java 面试八股文整理带答案及实操要点
本文整理了一线互联网大厂最新的高质量Java面试八股文及答案,涵盖Java基础、集合、多线程等多个核心方面,帮助你高效备考。内容包括面向对象与面向过程的区别、`equals`与`==`的对比、`final`和`static`的用法,以及ArrayList与LinkedList的区别、HashMap的工作原理等。同时,深入探讨了多线程创建方式、生命周期、上下文切换及死锁问题,并附有实操代码示例。资源链接:[点此下载](https://pan.quark.cn/s/14fcf913bae6)。
442 1
|
18天前
|
机器学习/深度学习 人工智能 安全
SentinelOne人工智能转折点 | 智能体与生成式人工智能如何重塑安全运营
生成式人工智能(GenAI)与智能体人工智能(Agentic AI)正加速赋能安全运营,助力企业提升威胁检测与响应能力。最新研究显示,96% 的 SOC 团队认为 AI 显著提升效率,近 70% 组织计划加大投入。AI 被广泛应用于威胁情报分析、工作流自动化与威胁狩猎等领域,推动安全运营从被动防御向主动管理转变。企业更关注 AI 解决方案的性能、上下文感知能力与数据隐私保障。未来,AI 将作为基石,助力构建更自主、高效、以人为本的安全运营中心(SOC)。
96 5