关于 Java 序列化的问题你真的会吗?
source link: https://www.infoq.cn/article/YqoD5WojTBkIHM989yDN
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
在持久化数据对象的时候我们很少使用 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(); } }
报错了,我们发现之前的 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)。
原文链接:
Recommend
-
70
iPhone - @rogwan - 虽然苹果官方否认了,当然这是官方必须的口吻。从产品和市场本身来看,好像删了,也没什么问题
-
9
产品经理需要自己的“敏捷开发”,你真的会吗? Wcof 2020-12-19 0 评论...
-
6
玩了多年借势营销,你真的会吗? 15天0基础极速入门数据分析,掌握一套数据分析流程和方法,学完就能写一份数据报告!了解一下>>...
-
4
用户召回你真的会吗?10%召回率仅需三步! | 产品壹佰 用户在每个产品中的生命周期是接触--使用--放弃或者遗忘的过程。在用户使用阶段,有效的促活手段也能提高留存,但同样重要的是召回用户,而召回用户有一个通用的流程。本...
-
7
摘要: 全景式解析NFT,从入门概念到上手实践 在 2021 年的头几个月里,非同质化代币(NFTs)步入了公众视野:佳士得和苏富比等美术拍卖行能够为数字艺术家 Beeple、Larva Lab 的团队等人的作品拍出高价。那么,为什么收藏家们要...
-
7
关于 Java 的可变参数你真的了解吗? ...
-
3
关于Wi-Fi 7你真的了解吗?且从高通的完整Wi-Fi 7生态看起 ...
-
6
求助,关于json反序列化问题 DG9Jww · 大约6小时之前 · 115 次点击 ·...
-
0
开源中国的红薯哥写了很多关于缓存的文章,其中多级缓存思路,分页列表缓存这些知识点给了我很大的启发性。 写这篇文章,我们聊聊分页列表缓存,希望能帮助大家提升缓存技术认知。 1 直接缓存分页列表...
-
7
V2EX › 数学 证明题!要过程(有人会吗) Tiaa
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK