关于 Java 序列化的问题你真的会吗?
云栖号:https://yqh.aliyun.com
第一手的上云资讯,不同行业精选的上云企业案例库,基于众多成功案例萃取而成的最佳实践,助力您上云决策!
在持久化数据对象的时候我们很少使用 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: hengheng password: 123456 序列化后数据: username: hengheng password: 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: hengheng password: 123456 序列化后数据: username: hengheng password: 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: hengheng password: 123456 序列化后数据: username: hengheng password: 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: hengheng password: 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(); } }
关于Java序列化的问题你真的会吗?
报错了,我们发现之前的 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: hengheng password: 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 序列化有了更多的了解?
云栖号在线课堂,每天都有产品技术专家分享
立即加入圈子:https://c.tb.cn/F3.Z8gvnK
与专家面对面,及时了解课程最新动态!
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
5种将死的编程语言
云栖号:https://yqh.aliyun.com第一手的上云资讯,不同行业精选的上云企业案例库,基于众多成功案例萃取而成的最佳实践,助力您上云决策! 现在的开发人员都趋向于使用新的编程语言,那么旧的编程语言呢?它们的前途一般是这样两种:仍然可以使用,但逐渐不受大家欢迎;直接完全死去。和之前的十佳最受欢迎的编程语言相反,本文我们预测以下这几种编程语言面临着死亡威胁: Perl 曾几何时,几乎每个人都在使用Perl语言编程。但是那些经常使用的人慢慢地发现,关于这个Perl语言似乎总是有点不对劲。至少我知道有这么个叫做“piecemeal”的编程语言,它的创造者似乎就只是将这个功能堆在另一个功能上面而已,并没有好好考虑将它们结合在一起。 事实上,甚至是它的创造者也不得不承认这种编程语言是有问题的。经过完整地改造之后,现在的开发工作开始倾向于使用Perl6,这个大概是在2000年的时候。至于Perl?俨然已经销声匿迹了!所以完全没有必要去学习它了。顺便说一句,下面这个“Goodbye World”就是用Perl写的: #!/usr/bin/perl print “Content-type:...
- 下一篇
Ubuntu 和 Raspbian 如何设置 Java Home
如何在 Ubuntu 和 Raspbian 中设置 JAVA_HOME 环境变量。 因为 Raspbian 的配置和 Ubuntu 差不多,如何进行配置。 首先你可以尝试 echo $JAVA_HOME 查看系统中的环境变量有没有配置。 如果没有输出则说明没有配置。 ![raspbian_jdk_home_01](https://yqfile.alicdn.com/2beb92a49b5459ff86cdfee89c7d31843b49e697.jpeg) 使用下面的命令对环境变量进行编辑。 sudo nano /etc/environment 在配置文件的最后输入下面的内容: JAVA_HOME="/usr/lib/jvm/default-java/" 如果 你不知道你安装的 JDK 版本是什么,路径是什么的话,请参考下面的文章:Ubuntu 如何查看安装的 JDK。 保存退出后,运行命令: source /etc/environment 这个命令的目的是将修改的配置应用到当前的用户中。 然后再运行 echo $JAVA_HOME 命令,你就可以看到当前的 JAVA_HOME 环境变...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS关闭SELinux安全模块
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- Mario游戏-低调大师作品
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Red5直播服务器,属于Java语言的直播服务器
- CentOS8编译安装MySQL8.0.19
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS6,CentOS7官方镜像安装Oracle11G