HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

关于 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:152850

评论 2 条评论

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

32岁的我裸辞了,八年Java老鸟,只因薪水被应届生倒挂,在闭关三个月后拿到阿里Offer,定级P7!

Java架构师迁哥

再获权威认可!DataPipeline实时数据融合平台通过信通院大数据产品能力评测

DataPipeline数见科技

大数据 数据融合 数据管理

Redis学习之Redis概述及原理、基本操作及持久化

Linux服务器开发

redis 分布式 后端 中间件 Linux服务器开发

爆款阿里P5到P7晋升之路,九大源码文档助我超神果然努力幸运并存

Java BAT

5分钟学会本地Pypi源搭建

Python研究所

Python

越学越有趣:『手把手带你学NLP』系列项目07 ——机器翻译的那些事儿

百度大脑

机器学习 nlp

2021百度云智峰会|DataPipeline携手百度赋能实时数据资产构建与应用

DataPipeline数见科技

大数据 数据融合 数据管理

图的应用——拓扑排序

若尘

数据结构 拓扑排序

我是如何从设计师转到软件行业的? | 可怜巴巴的程序猿

Python研究所

随笔 话题讨论

2021年,BAT接连入局!“低代码”为何能备受资本追捧?

优秀

低代码

阿里P8手抄本惨遭泄露,并出现病毒式传播,致28人斩获大厂offer

Java架构师迁哥

技术干货 | 录屏采集实现教程 —— Android端

ZEGO即构

大前端 音视频 录屏采集 Android端

免费分享JDBC与MyBatis的优秀图书

Java入门到架构

Java

TDengine JDBC整合Druid

山石道人

涛思数据 tdengine Druid Spring MVC taos-jdbc

接任务时一定要锁定需求啊!(转自小明同学)

BinTools图尔兹

数据库 dba 国产数据库 运维开发

一周信创舆情观察(6.28~7.4)

统小信uos

用好“实时数据管理”助推器,旭辉集团加快数字化转型步伐

DataPipeline数见科技

大数据 数据融合 数据管理

“用友 • 华为云杯”第三届企业云服务开发者大赛

ToB行业头条

开发者 低代码 用友

“京东商城”亿级高并发秒杀系统到底是怎么设计的?自己做该如何下手?

Java架构师迁哥

【架构实战营】第 7 模块作业

swordman

架构实战营

聊聊事务与分布式系统-从零讲到通透

刘绍

sql 分布式 事务 2PC XA

Hightopo可视化入局“智慧工厂”,助力企业改革创新

一只数据鲸鱼

数据可视化 绿色工业 3D数据可视化 高炉炼铁 智慧工业

Mobileye智慧出行再加码,中国市场生态建设取得新进展

E科讯

MindSpore教程免费学,还有入门奖品

Geek_6cdeb6

人工智能 机器学习 深度学习 自我提升 羊毛

北鲲云超算平台——让科技更好地服务于用户

北鲲云

DataPipeline实时数据融合平台V3.0里程碑版发布!澎湃新动能

DataPipeline数见科技

大数据 数据融合 数据管理

一图读懂DataPipeline实时数据融合平台V3.0

DataPipeline数见科技

大数据 数据融合 数据管理

半年摸爬滚打,我是如何从小白进阶渗透测试工程师

网络安全学海

黑客 网络安全 信息安全 渗透测试 漏洞分析

云南智慧公安研判分析系统搭建,重点人员研判分析平台

构筑智能制造时代“四重护城河”,联想中国开创产业新格局

科技大数据

正式发布!中国首个LF Edge捐赠项目Baetyl 2.2发布

百度开发者中心

物联网 开源技术

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