阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

关于 Java 序列化的问题你真的会吗?

  • 2020-03-12
  • 本文字数:5602 字

    阅读完需:约 18 分钟

关于Java序列化的问题你真的会吗?

在持久化数据对象的时候我们很少使用 Java 序列化,而是使用数据库等方式来实现。但是在我看来,Java 序列化是一个很重要的内容,序列化不仅可以保存对象到磁盘进行持久化,还可以通过网络传输。在平时的面试当中,序列化也是经常被谈及的一块内容。


谈到序列化时,大家可能知道将类实现 Serializable 接口就可以达到序列化的目的,但当看到关于序列化的面试题时我们却常常一脸懵逼。


1)可序列化接口和可外部接口的区别是什么?


2)序列化时,你希望某些成员不要序列化?该如何实现?


3)什么是 serialVersionUID ?如果不定义 serialVersionUID,会发生什么?


是不是突然发现我们对这些问题其实都还存在很多疑惑?本文将总结一些 Java 序列化的常见问题,并且通过 demo 来进行测试和解答

一、什么是 Java 序列化?

序列化是把对象改成可以存到磁盘或通过网络发送到其它运行中的 Java 虚拟机的二进制格式的过程,并可以通过反序列化恢复对象状态。Java 序列化 API 给开发人员提供了一个标准机制:通过实现 java.io.Serializable 或者 java.io.Externalizable 接口,ObjectInputStream 及 ObjectOutputStream 处理对象序列化。实现 java.io.Externalizable 接口的话,Java 程序员可自由选择基于类结构的标准序列化或是它们自定义的二进制格式,通常认为后者才是最佳实践,因为序列化的二进制文件格式成为类输出 API 的一部分,可能破坏 Java 中私有和包可见的属性的封装。


序列化到底有什么用


实现 java.io.Serializable


定义用户类:


class User implements Serializable {    private String username;    private String passwd;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPasswd() { return passwd; }
public void setPasswd(String passwd) { this.passwd = passwd; }}
复制代码


我们把对象序列化,通过 ObjectOutputStream 存储到 txt 文件中,再通过 ObjectInputStream 读取 txt 文件,反序列化成 User 对象。


public class TestSerialize {
public static void main(String[] args) {
User user = new User(); user.setUsername("hengheng"); user.setPasswd("123456");
System.out.println("read before Serializable: "); System.out.println("username: " + user.getUsername()); System.err.println("password: " + user.getPasswd());
try { ObjectOutputStream os = new ObjectOutputStream( new FileOutputStream("/Users/admin/Desktop/test/user.txt")); os.writeObject(user); // 将User对象写进文件 os.flush(); os.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } try { ObjectInputStream is = new ObjectInputStream(new FileInputStream( "/Users/admin/Desktop/test/user.txt")); user = (User) is.readObject(); // 从流中读取User的数据 is.close();
System.out.println("\nread after Serializable: "); System.out.println("username: " + user.getUsername()); System.err.println("password: " + user.getPasswd());
} catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }}
复制代码


运行结果如下:


序列化前数据:username: henghengpassword: 123456
序列化后数据:username: henghengpassword: 123456
复制代码


到这里,我们大概知道了什么是序列化。

二、序列化时如何保证某些成员不被序列化?

答案:声明该成员为静态或瞬态,在 Java 序列化过程中则不会被序列化


  • 静态变量:加 static 关键字。

  • 瞬态变量:加 transient 关键字。


我们先尝试把变量声明为瞬态。


class User implements Serializable {    private String username;    private transient String passwd;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPasswd() { return passwd; }
public void setPasswd(String passwd) { this.passwd = passwd; }
复制代码


在密码字段前加上了 transient 关键字再运行。运行结果:


序列化前数据:username: henghengpassword: 123456
序列化后数据:username: henghengpassword: null
复制代码


通过运行结果发现密码没有被序列化,达到了我们的目的。


再尝试在用户名前加 static 关键字。


class User implements Serializable {    private static String username;    private transient String passwd;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPasswd() { return passwd; }
public void setPasswd(String passwd) { this.passwd = passwd; }
复制代码


运行结果:


序列化前数据:username: henghengpassword: 123456
序列化后数据:username: henghengpassword: null
复制代码


我们发现运行后的结果和预期的不一样,按理说 username 也应该变为 null 才对。是什么原因呢?


原因是:反序列化后类中 static 型变量 username 的值为当前 JVM 中对应的静态变量的值,而不是反序列化得出的


我们来证明一下:


public class TestSerialize {
public static void main(String[] args) {
User user = new User(); user.setUsername("hengheng"); user.setPasswd("123456");
System.out.println("序列化前数据: "); System.out.println("username: " + user.getUsername()); System.err.println("password: " + user.getPasswd());
try { ObjectOutputStream os = new ObjectOutputStream( new FileOutputStream("/Users/admin/Desktop/test/user.txt")); os.writeObject(user); // 将User对象写进文件 os.flush(); os.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } User.username = "小明"; try { ObjectInputStream is = new ObjectInputStream(new FileInputStream( "/Users/admin/Desktop/test/user.txt")); user = (User) is.readObject(); // 从流中读取User的数据 is.close();
System.out.println("\n序列化后数据: "); System.out.println("username: " + user.getUsername()); System.err.println("password: " + user.getPasswd());
} catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }}
class User implements Serializable { public static String username; private transient String passwd;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPasswd() { return passwd; }
public void setPasswd(String passwd) { this.passwd = passwd; }}
复制代码


在反序列化前把静态变量 username 的值改为『小明』。


User.username = "小明";
复制代码


再运行一次:


序列化前数据:username: henghengpassword: 123456
序列化后数据:username: 小明password: null
复制代码


果然,这里的 username 是 JVM 中静态变量的值,并不是反序列化得到的值。

三、serialVersionUID 有什么用?

我们经常会在类中自定义一个 serialVersionUID:


private static final long serialVersionUID = 8294180014912103005L
复制代码


这个 serialVersionUID 有什么用呢?如果不设置的话会有什么后果?


serialVersionUID 是一个 private static final long 型 ID,当它被印在对象上时,它通常是对象的哈希码。serialVersionUID 可以自己定义,也可以自己去生成


不指定 serialVersionUID 的后果是:当你添加或修改类中的任何字段时,已序列化类将无法恢复,因为新类和旧序列化对象生成的 serialVersionUID 将有所不同。Java 序列化的过程是依赖于正确的序列化对象恢复状态的,并在序列化对象序列版本不匹配的情况下引发 java.io.InvalidClassException 无效类异常。


举个例子大家就明白了:


我们保持之前保存的序列化文件不变,然后修改 User 类。


class User implements Serializable {    public static String username;    private transient String passwd;    private String age;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPasswd() { return passwd; }
public void setPasswd(String passwd) { this.passwd = passwd; }
public String getAge() { return age; }
public void setAge(String age) { this.age = age; }}
复制代码


加了一个属性 age,然后单另写一个反序列化的方法:


public static void main(String[] args) {        try {            ObjectInputStream is = new ObjectInputStream(new FileInputStream(                    "/Users/admin/Desktop/test/user.txt"));            User user = (User) is.readObject(); // 从流中读取User的数据            is.close();
System.out.println("\n修改User类之后的数据: "); System.out.println("username: " + user.getUsername()); System.err.println("password: " + user.getPasswd());
} catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
复制代码



报错了,我们发现之前的 User 类生成的 serialVersionUID 和修改后的 serialVersionUID 不一样(因为是通过对象的哈希码生成的),导致了 InvalidClassException 异常。


自定义 serialVersionUID:


class User implements Serializable {    private static final long serialVersionUID = 4348344328769804325L;
public static String username; private transient String passwd; private String age;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPasswd() { return passwd; }
public void setPasswd(String passwd) { this.passwd = passwd; }
public String getAge() { return age; }
public void setAge(String age) { this.age = age; }}
复制代码


再试一下:


序列化前数据:username: henghengpassword: 123456
序列化后数据:username: 小明password: null
复制代码


运行结果无报错,所以一般都要自定义 serialVersionUID。

四、是否可以自定义序列化过程?

答案当然是可以的


之前我们介绍了序列化的第二种方式:


实现 Externalizable 接口,然后重写 writeExternal() 和 readExternal()方法,这样就可以自定义序列化


比如我们尝试把变量设为瞬态。


public class ExternalizableTest implements Externalizable {
private transient String content = "我是被transient修饰的变量哦";
@Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(content); }
@Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { content = (String) in.readObject(); }
public static void main(String[] args) throws Exception {
ExternalizableTest et = new ExternalizableTest(); ObjectOutput out = new ObjectOutputStream(new FileOutputStream( new File("test"))); out.writeObject(et);
ObjectInput in = new ObjectInputStream(new FileInputStream(new File( "test"))); et = (ExternalizableTest) in.readObject(); System.out.println(et.content);
out.close(); in.close(); }}
复制代码


运行结果:


我是被transient修饰的变量哦
复制代码


这里实现的是 Externalizable 接口,则没有任何东西可以自动序列化,需要在 writeExternal 方法中进行手工指定所要序列化的变量,这与是否被 transient 修饰无关。


通过上述介绍,是不是对 Java 序列化有了更多的了解?


本文转载自公众号宜信技术学院(ID:CE_TECH)。


原文链接


https://mp.weixin.qq.com/s/7IucJdHLfCe4CmwH5h7Axw


2020-03-12 10:152724

评论 2 条评论

发布
用户头像
广义上,序列化是将内存模型转为约定好的数据结构的数据模型,为什么非得要序列化成字节码?json字符串不好?
2020-04-04 19:03
回复
用户头像
实现 java.io.Externalizable 接口的话,Java 程序员可自由选择基于类结构的标准序列化或是它们自定义的二进制格式,通常认为后者才是最佳实践,因为序列化的二进制文件格式成为类输出 API 的一部分,可能破坏 Java 中私有和包可见的属性的封装。 这句话应该是前者吧
2020-03-13 10:23
回复
没有更多了
发现更多内容

Debian11系统编译安装Redis教程。

百度搜索:蓝易云

redis 云计算 Linux 运维 Debian

MobPush Android For Unity

MobTech袤博科技

开发者 前端 Unity Android; Java’

只有1%的人才知道的ChatGPT写作技巧

俞凡

人工智能 ChatGPT

IPQ8072|XGS-PON|Dual Band 10GbE Wifi6 Industrial SBC DR8072V01

wallyslilly

当你成为一个Tech Lead

码猿外

技术管理 Tech Lead

引领AI变革,九章云极DataCanvas公司重磅发布AIFS+DataPilot

九章云极DataCanvas

完成等保测评后有合格证书吗?是什么样的?

行云管家

等保测评 等保2.0 等级测评

Debian11系统编译安装Memcached教程。

百度搜索:蓝易云

memcached 云计算 Linux 运维 Debian

用ChatGPT挣钱的五种思路

高端章鱼哥

机器人 零售行业 ChatGPT

认识高性能服务治理框架 Kmesh

openEuler

Linux 开源 操作系统 openEuler 服务网格

高性能网络设计秘笈:深入剖析Linux网络IO与epoll

华为云开发者联盟

后端 开发 华为云 华为云开发者联盟 企业号 7 月 PK 榜

缕析条分Scroll属性 | 京东云技术团队

京东科技开发者

前端 DOM ScrollView ScrollView(滚动条) 企业号 7 月 PK 榜

Last Week in Milvus

Zilliz

Milvus Zilliz AIGC cvpstack

ChatGPT火热之下的冷思考

这我可不懂

AIGC ChatGPT

小动作牵动大文明,“大运空瓶行动”从你我做起

新消费日报

LeetCode题解:2618. 检查是否是类的对象实例,使用instanceof

Lee Chen

JavaScript LeetCode

Win11 搭建SD WebUI环境 | 社区征文

IT蜗壳-Tango

AIGC Stable Diffusion 年中技术盘点 SD

航空机场行业如何绘就全面预算降本增效新画卷?

用友BIP

全面预算

火山引擎DataLeap如何解决SLA治理难题(二):申报签署流程与复盘详解

字节跳动数据平台

大数据 数据中台 数据研发

亚信科技荣任「DBL电信行业工作组」副组长单位,AntDB数据库连年入选《中国数据库产品图谱》

亚信AntDB数据库

AntDB 数据库· AntDB数据库 企业号 7 月 PK 榜

聊聊Spring注解@Transactional失效的那些事 | 京东云技术团队

京东科技开发者

spring Transactional @Transactional 企业号 7 月 PK 榜 注解失效

DDD架构为什么应该首选六边形架构? | 京东云技术团队

京东科技开发者

分层架构 架构设计 企业号 7 月 PK 榜 六边形架构

升级数智底座是数智化2.0时代的核心诉求

用友BIP

数智底座

国赛线下开赛!全国智能车百度智慧交通创意组区域赛今日正式拉开帷幕!

飞桨PaddlePaddle

人工智能 百度 paddle 飞桨 百度飞桨

Brotli-压缩算法的潮流 | 社区征文

不叫猫先生

Brotli 压缩算法 年中技术盘点

直播软件源码开发搭建提高安全性方案——山东布谷科技创作

山东布谷科技

源码 软件 软件开发 直播 源码搭建

ChatGPT赋能Scrum实践

俞凡

人工智能 Scrum 敏捷开发 ChatGPT

安装Ingress-Nginx

tiandizhiguai

云原生 k8s

浅谈一下自动化运维优点和缺点,哪款工具好?

行云管家

自动化 IT运维 自动化运维

ChatGPT的探索与实践-业务应用篇 | 京东云技术团队

京东科技开发者

人工智能 ChatGPT 企业号 7 月 PK 榜

一文让你了解全媒体广告投放。轻轻松松搞定全媒体信息流广告投放代理!

吴老师讲业

全媒体互联网广告 全媒体广告投放代理 招商加盟 信息流广告

关于Java序列化的问题你真的会吗?_数据库_杨亨_InfoQ精选文章