写点什么

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

评论 2 条评论

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

图像分类-cifar100 实验研究

毛显新

人工智能 神经网络 tensorflow 图像识别 keras

AI巨头们建造的“新世界”,进展如何?

脑极体

OceanBase 源码解读(三)分区的一生

OceanBase 数据库

数据库 分布式数据库 oceanbase OceanBase 开源 OceanBase 社区版

☕【Java技术指南】「OpenJDK专题」想不想编译属于你自己的JDK呢?(Windows10环境)

码界西柚

Java jdk Openjdk 8月日更

搜索引擎渐行渐远,未来路在何方

石头IT视角

百度地图开发-实现离线地图功能 05

Andy阿辉

android 百度地图 Android 小菜鸟 Android端 8月日更

学习心得-架构训练营-第一课

Fm

微信业务架构图-作业

Geek_a772a7

学生管理系统(作业)

Geek_a772a7

模块一作业

potti

架构实战营

HTTP接口测试基础【FunTester框架教程】

FunTester

自动化测试 教程 接口测试 测试框架 FunTester

仓储执行系统(WES)

申扬科技

WCS wms WES 仓储执行系统

网络安全小白别拜师了,求人不如求己

网络安全学海

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

Flutter 安卓 Platform 与 Dart 端消息通信方式 Channel 源码解析

工匠若水

flutter android 8月日更

01. 你身边的AI

Databri_AI

人工智能

架构训练营模块一作业

guangbao

架构实战营模块六作业

老猎人

架构实战营

架构训练营 模块一作业

初一

【DPDK工程师手册】 —— 官方文档,最新视频,开源项目,论文,大厂内部ppt,知名工程师一览表

奔着腾讯去

Linux DPDK VPP

正经人一辈子都用不到的 JavaScript 方法总结 (二)

编程三昧

JavaScript 大前端 8月日更

graphql中的'子查询'

杜艮魁

开源 后端 graphql

Linux之nohup命令

入门小站

Linux

微信的业务架构图

Rabbit

架构实战营

极客时间【架构实战营】第二期 模块一作业

Geek_91606e

架构实战营

[架构实战营]模块一

Amy

架构实战营 业务架构图

作业

Li. Mr

在线JSON转XML工具

入门小站

工具

初识html,一文搞懂HTMl骨架标签都有哪些含义及浏览器内核

你好bk

html html5 大前端 浏览器 html/css

架构师实战营作业[模块一]

看,有只猪

python通过PyQt5实现登录界面

Python研究者

8月日更

🚀【Guava技术指南】「RateLimiter类」服务请求流控实现方案

码界西柚

Java ratelimiter Guava 8月日更

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