NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

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

评论 2 条评论

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

模块七作业

心怀架构

有向无环图在新增节点时的环状检测

waitmoon

DAG

业界福音!快来使用Amazon Transcribe服务减轻添加字幕的繁琐工序负担吧

亚马逊云科技 (Amazon Web Services)

AI/ML

农业与科技结合?快来看Amazon Rekognition自定义标签的作用吧

亚马逊云科技 (Amazon Web Services)

AI/ML

动手训练属于自己的无人车,这个超强服务现已开源!

亚马逊云科技 (Amazon Web Services)

AI/ML

模块7作业

小何

「架构实战营」

Redis RDB 持久化详解

程序员历小冰

redis 持久化 28天写作 12月日更

在Amazon SageMaker中灵活使用多种存储服务

亚马逊云科技 (Amazon Web Services)

AI/ML

如何真正学习Go 语言

宇宙之一粟

golang 学习方法 Go 语言 12月日更

使用 Amazon IoT 和 Amazon SageMaker 进行设备实时预测性维护

亚马逊云科技 (Amazon Web Services)

AI/ML

SageMaker Neo优化目标检测模型加速推理

亚马逊云科技 (Amazon Web Services)

AI/ML

YB时代即将来临,三问数据存储

脑极体

apacheunomi漏洞介绍及代码分析

网络安全学海

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

☕【Java实战系列】「技术盲区」Double与Float的坑与解决办法以及BigDecimal的取而代之!

洛神灬殇

BigDecimal Java 开发 12月日更 Double和Float

巧用机器学习托管服务,自动化合约处理从此不在话下!

亚马逊云科技 (Amazon Web Services)

AI/ML

低代码实现探索(九)后台模型 json定义

零道云-混合式低代码平台

区块链数字版权,区块链数字藏品交易系统开发

a13823115807

#区块链# 区块链技术应用 区块链数字藏品

架构实战营模块七课后作业

Geek_99eefd

#架构实战营 「架构实战营」

深入浅出 OceanBase 运维之弹性扩缩容

OceanBase 数据库

直播 弹性扩容 OceanBase 开源

太香了,终于有人耗时1000小时打造出python从入门到精通全套路线图+视频+笔记

Java全栈架构师

Python 数据库 架构 面试 程序人生

如何让用户给我们做推荐?

石云升

AARRR 产品思维 28天写作 12月日更

25天,手码Python数据分析+八大核心项目实战25W字总结,我献出了我的膝盖

Java全栈架构师

Python 数据挖掘 程序员 架构 数据分析

即刻到位!快速落地 Amazon 智能工厂解决方案

亚马逊云科技 (Amazon Web Services)

AI/ML

重装上阵——Graviton2提升ElastiCache for Redis的性价比!

亚马逊云科技 (Amazon Web Services)

AI/ML

编写代码最应该做好的事情是什么?

李子捌

Java 28天写作 21天挑战 12月日更

新能源当道,如何构建一个电动汽车电池告警预测平台?

亚马逊云科技 (Amazon Web Services)

AI/ML

微众七年营造,ABCD“四梁八柱”建构数字时代的信任底座

脑极体

销售易携手DataPipeline,推动“实时感知-主动决策-客户成功”的变革!

DataPipeline数见科技

大数据 中间件 数据融合 数据迁移 数据管理

飞桨中国行——生产制造专场

百度大脑

人工智能

开源驱动未来 | 2021新一代人工智能院士高峰论坛暨Open/O启智开发者大会开源专场顺利召开

OpenI启智社区

人工智能 开源社区 启智开发者大会

首次开源!一行代码轻松搞定中英文语音识别、合成、翻译核心功能!

百度大脑

人工智能

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