关于 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。

定义用户类:

复制代码

classUserimplementsSerializable{
privateStringusername;
privateStringpasswd;

publicStringgetUsername(){
returnusername;
}

publicvoidsetUsername(Stringusername){
this.username = username;
}

publicStringgetPasswd(){
returnpasswd;
}

publicvoidsetPasswd(Stringpasswd){
this.passwd = passwd;
}
}

我们把对象序列化,通过 ObjectOutputStream 存储到 txt 文件中,再通过 ObjectInputStream 读取 txt 文件,反序列化成 User 对象。

复制代码

publicclassTestSerialize {

publicstaticvoidmain(String[] args) {

Useruser=newUser();
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 =newObjectOutputStream(
newFileOutputStream("/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 {
ObjectInputStreamis=newObjectInputStream(newFileInputStream(
"/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 关键字。

我们先尝试把变量声明为瞬态。

复制代码

classUserimplementsSerializable{
privateStringusername;
privatetransientStringpasswd;

publicStringgetUsername(){
returnusername;
}

publicvoidsetUsername(Stringusername){
this.username = username;
}

publicStringgetPasswd(){
returnpasswd;
}

publicvoidsetPasswd(Stringpasswd){
this.passwd = passwd;
}

在密码字段前加上了 transient 关键字再运行。运行结果:

复制代码

序列化前数据:
username:hengheng
password:123456

序列化后数据:
username:hengheng
password:null

通过运行结果发现密码没有被序列化,达到了我们的目的。

再尝试在用户名前加 static 关键字。

复制代码

classUserimplementsSerializable{
privatestaticStringusername;
privatetransientStringpasswd;

publicStringgetUsername(){
returnusername;
}

publicvoidsetUsername(Stringusername){
this.username = username;
}

publicStringgetPasswd(){
returnpasswd;
}

publicvoidsetPasswd(Stringpasswd){
this.passwd = passwd;
}

运行结果:

复制代码

序列化前数据:
username:hengheng
password:123456

序列化后数据:
username:hengheng
password:null

我们发现运行后的结果和预期的不一样,按理说 username 也应该变为 null 才对。是什么原因呢?

原因是:反序列化后类中 static 型变量 username 的值为当前 JVM 中对应的静态变量的值,而不是反序列化得出的。

我们来证明一下:

复制代码

publicclassTestSerialize{

publicstaticvoidmain(String[] args){

User user =newUser();
user.setUsername("hengheng");
user.setPasswd("123456");

System.out.println(" 序列化前数据: ");
System.out.println("username: "+ user.getUsername());
System.err.println("password: "+ user.getPasswd());

try{
ObjectOutputStream os =newObjectOutputStream(
newFileOutputStream("/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 =newObjectInputStream(newFileInputStream(
"/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();
}
}
}

classUserimplementsSerializable{
publicstaticStringusername;
privatetransientStringpasswd;

publicStringgetUsername(){
returnusername;
}

publicvoidsetUsername(Stringusername){
this.username = username;
}

publicStringgetPasswd(){
returnpasswd;
}

publicvoidsetPasswd(Stringpasswd){
this.passwd = passwd;
}
}

在反序列化前把静态变量 username 的值改为『小明』。

复制代码

User.username=" 小明 ";

再运行一次:

复制代码

序列化前数据:
username:hengheng
password:123456

序列化后数据:
username:小明
password:null

果然,这里的 username 是 JVM 中静态变量的值,并不是反序列化得到的值。

三、serialVersionUID 有什么用?

我们经常会在类中自定义一个 serialVersionUID:

复制代码

privatestaticfinallongserialVersionUID =8294180014912103005L

这个 serialVersionUID 有什么用呢?如果不设置的话会有什么后果?

serialVersionUID 是一个 private static final long 型 ID,当它被印在对象上时,它通常是对象的哈希码。serialVersionUID 可以自己定义,也可以自己去生成。

不指定 serialVersionUID 的后果是:当你添加或修改类中的任何字段时,已序列化类将无法恢复,因为新类和旧序列化对象生成的 serialVersionUID 将有所不同。Java 序列化的过程是依赖于正确的序列化对象恢复状态的,并在序列化对象序列版本不匹配的情况下引发 java.io.InvalidClassException 无效类异常。

举个例子大家就明白了:

我们保持之前保存的序列化文件不变,然后修改 User 类。

复制代码

classUserimplementsSerializable{
publicstaticStringusername;
privatetransientStringpasswd;
privateStringage;

publicStringgetUsername(){
returnusername;
}

publicvoidsetUsername(Stringusername){
this.username = username;
}

publicStringgetPasswd(){
returnpasswd;
}

publicvoidsetPasswd(Stringpasswd){
this.passwd = passwd;
}

publicStringgetAge(){
returnage;
}

publicvoidsetAge(Stringage){
this.age = age;
}
}

加了一个属性 age,然后单另写一个反序列化的方法:

复制代码

publicstaticvoidmain(String[] args) {
try {
ObjectInputStreamis=newObjectInputStream(newFileInputStream(
"/Users/admin/Desktop/test/user.txt"));
Useruser= (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:

复制代码

classUserimplementsSerializable{
privatestaticfinallongserialVersionUID =4348344328769804325L;

publicstaticStringusername;
privatetransientStringpasswd;
privateStringage;

publicStringgetUsername(){
returnusername;
}

publicvoidsetUsername(Stringusername){
this.username = username;
}

publicStringgetPasswd(){
returnpasswd;
}

publicvoidsetPasswd(Stringpasswd){
this.passwd = passwd;
}

publicStringgetAge(){
returnage;
}

publicvoidsetAge(Stringage){
this.age = age;
}
}

再试一下:

复制代码

序列化前数据:
username:hengheng
password:123456

序列化后数据:
username:小明
password:null

运行结果无报错,所以一般都要自定义 serialVersionUID。

四、是否可以自定义序列化过程?

答案当然是可以的。

之前我们介绍了序列化的第二种方式:

实现 Externalizable 接口,然后重写 writeExternal() 和 readExternal() 方法,这样就可以自定义序列化。

比如我们尝试把变量设为瞬态。

复制代码

publicclassExternalizableTestimplementsExternalizable{

privatetransientString content =" 我是被 transient 修饰的变量哦 ";

@Override
publicvoidwriteExternal(ObjectOutput out)throwsIOException{
out.writeObject(content);
}

@Override
publicvoidreadExternal(ObjectInput in)throwsIOException,
ClassNotFoundException{
content = (String) in.readObject();
}

publicstaticvoidmain(String[] args)throwsException{

ExternalizableTest et =newExternalizableTest();
ObjectOutput out =newObjectOutputStream(newFileOutputStream(
newFile("test")));
out.writeObject(et);

ObjectInput in =newObjectInputStream(newFileInputStream(newFile(
"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

原文 

https://www.infoq.cn/article/YqoD5WojTBkIHM989yDN

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » 关于 Java 序列化的问题你真的会吗?

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址