速来报名!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:152866

评论 2 条评论

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

cleanmymac x4.13.2中文版mac清理软件

茶色酒

CleanMyMac X

FL Studio 2023中文版水果音乐制作软件

茶色酒

FL Studio 21

FL Studio21最新数字音频工作站(DAW)软件

茶色酒

FL Studio21

Docker安全中的一些技术工具

穿过生命散发芬芳

Docker 三周年连更

得物直播低延迟探索 | 得物技术

得物技术

直播技术 直播推流 直播优化

MES系统中的BOM为何如此重要?先进的BOM体系怎么建立?

优秀

MES系统 BOM

EasyRecovery2024免费版电脑数据恢复软件

茶色酒

EasyRecovery15

【分布式技术专题】「分布式技术架构」手把手教你如何开发一个属于自己的分布式锁的功能组件

洛神灬殇

redis 分布式锁 三周年连更 动手实现

简介一下LoRA模型

IT蜗壳-Tango

三周年连更

我的职业规划 |社区征文

来碗老郭

三周年征文

2023-05-04:用go语言重写ffmpeg的scaling_video.c示例,用于实现视频缩放(Scaling)功能。

福大大架构师每日一题

golang 音视频 流媒体 福大大架构师每日一题

FLstudio2024最新版水果编曲软件

茶色酒

FLStudio21

瓴羊QUICKBI工具帮助企业处理数据,效率提升数倍!

对不起该用户已成仙‖

Paragon NTFS2023最新版ntfs磁盘读写工具

茶色酒

Paragon NTFS

分布式Mysql

乌龟哥哥

三周年连更

Flink运行架构

阿泽🧸

flink 三周年连更

Stable Diffusion WebUI汉化

IT蜗壳-Tango

三周年连更

【Python实战】Python采集图片数据

BROKEN

三周年连更

论存储在智算与超算平台建设中的重要性

焱融科技

#分布式文件存储 #全闪存储 #高性能存储

如何参与Cetus和Oi! Network联合ISO认购和空投奖励?

股市老人

文心一言 VS chatgpt (14)-- 算法导论3.2 2~3题

福大大架构师每日一题

ChatGPT 文心一言 福大大架构师每日一题

Syslog-ng RHEL 的安装和配置

HoneyMoose

JS中数组随机排序实现(原地算法sort/shuffle算法)

不叫猫先生

JavaScrip 算法、 三周年连更

Shell的基本使用方法

梦笔生花

shell脚本编程 Shell命令 三周年连更

登顶Nature 正刊!百度生物计算用AI首次实现mRNA领域重大进展

脑极体

AI 生物

Go常用设计模式(上)

闫同学

三周年连更

从入门到放弃再到成功—我的 Jira 插件探索之路

跟YY哥学Jira

Scrum cli Jira插件 Forge 团队速度

选择无服务器:Babbel 的迁移故事

亚马逊云科技 (Amazon Web Services)

致敬青年——开源的生力军

开放原子开源基金会

开放原子开源基金会

Guitar Pro2023专业的吉他制谱软件

茶色酒

Guitar Pro8

Django笔记二十一之使用原生SQL查询数据库

Hunter熊

Python sql django connection

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