【总结】你所不知道的Java序列化
我们都知道,Java序列化可以让我们记录下运行时的对象状态(对象实例域的值),也就是我们经常说的对象持久化 。这个过程其实是非常复杂的,这里我们就好好理解一下Java的对象序列化。
1、 首先我们要搞清楚,Java对象序列化是将 对象的实例域数据( 包括private私有域) 进行持久化存储。而并非是将整个对象所属的类信息进行存储。 其实了解JVM的话,我们就能明白这一点了。实际上堆中所存储的对象包含了实例域数据值以及指向类信息的地址,而对象所属的类信息却存放在方法区中。当我们要对持久层数据反序列化成对象的时候,也就只需要将实例域数据值存放在新创建的对象中即可。
2、 我们都知道凡要序列化的类都必须实现Serializable接口。 但是不是所有类都可以序列化呢?当然不是这样,想想看序列化可以让我们轻而易举的接触到对象的私有数据域,这是多么危险的漏洞呀!总结一下,JDK中有四种类型的类对象是绝对不能序列化的 。
(1) 太依赖于底层实现的类(too closely tied to native code)。比如java.util.zip.Deflater。
(2) 对象的状态依赖于虚拟机内部和不停变化的运行时环境。比如java.lang.Thread, java.io.InputStream
(3) 涉及到潜在的安全性问题。比如:java.lang.SecurityManager, java.security.MessageDigest
(4) 全是静态域的类,没有对象实例数据。要知道静态域本身也是存储在方法区中的。
3、 自定义的类只要实现了Serializable接口,是不是都可以序列化呢? 当然也不是这样,看看下面的例子:
- class Employee implements Serializable{
- private ZipFile zf=null;
- Employee(ZipFile zf){
- this.zf=zf;
- }
- }
- ObjectOutputStream oout=
- new ObjectOutputStream(new FileInputStream(new File("aaa.txt")));
- oout.writeObject(new Employee(new ZipFile("c://.."));
我们会发现运行之后抛出java.io.NotSerializableException : java.util.zip.ZipFile 。很明显,如果要对Employee对象序列化,就必须对其数据域ZipFile对象也进行序列化,而这个类在JDK中是不可序列化的。因此,包含了不可序列化的对象域的对象也是不能序列化的。 实际上,这也并非不可能,我们在下面第6点会谈到。
4、 可序列化的类成功序列化之后,是不是一定可以反序列化呢? (这里默认在同一环境下,而且类定义永远不会改变,即满足兼容性。在下面我们会讨论序列化的不兼容性)。答案是不一定哦!我们还是看一个列子:
- //父类对象不能序列化
- class Employee{
- private String name;
- Employee(String n){
- this.name=n;
- }
- public String getName(){
- return this.name;
- }
- }
- //子类对象可以序列化
- class Manager extends Employee implements Serializable{
- private int id;
- Manager(String name, int id){
- super(name);
- this.id=id;
- }
- }
- //序列化与反序列化测试
- public static void main(String[] args) throws IOException, ClassNotFoundException{
- File file=new File("E:/aaa.txt");
- ObjectOutputStream oout=new ObjectOutputStream(new FileOutputStream(file));
- oout.writeObject(new Manager("amao",123));
- oout.close();
- System.out.println("序列化成功");
- ObjectInputStream oin=new ObjectInputStream(new FileInputStream(file));
- Object o=oin.readObject();
- oin.close();
- System.out.println("反序列化成功:"+((Manager) o).getName());
- }
程序的运行结果是:打印出“序列化成功”之后抛出java.io.InvalidClassException: Manager; Manager; no valid constructor。 为什么会出现这种情况呢?很显然,序列化的时候只是将Manager类对象的数据域id写入了文件,但在反序列化的过程中,需要在堆中建立一个Manager新对象。我们都知道任何一个类对象的建立都首先需要调用父类的构造器对父类进行初始化,很可惜序列化文件中并没有父类Employee的name数据,那么此时调用Employee(String)构造器会因为没有数据而出现异常。既然没有数据,那么可不可以调用无参构造器呢? 事实却是如此,如果有Employee()无参构造器的存在,将不会抛出异常,只是在执行打印的时候出现--- “反序列化成功:null”。
总结一下:如果当前类的所有超类中有一个类即不能序列化,也没有无参构造器。那么当前类将不能反序列化。如果有无参构造器,那么此超类反序列化的数据域将会是null或者0,false等等。
5、 序列化的兼容性问题!
类定义很有可能在不停的人为更新(比如JDK1.1到JDK1.2中HashTable的改变)。那么以前序列化的旧类对象很可能不能再反序列化成为新类对象。这就是序列化的兼容性问题,严格意义上来说改变类中除static 和transient以外的所有部分都会造成兼容性问题。而JDK采用了一种stream unique identifier (SUID) 来识别兼容性。SUID是通过复杂的函数来计算的类名,接口名,方法和数据域的 一个64位 hash值。而这个值存储在类中的静态域内:
private static final long serialVersionUID = 3487495895819393L
只要稍微改动类的定义,这个类的SUID就会发生变化,我们通过下面的程序来看看:
- //修改前的Employee
- class Employee implements Serializable{
- private String name;
- Employee(String n){
- this.name=n;
- }
- public String getName(){
- return this.name;
- }
- }
- //测试,打印SUID=5135178525467874279L
- long serialVersionUID=ObjectStreamClass.lookup(Class.forName("Employee")).getSerialVersionUID();
- System.out.println(serialVersionUID);
- //修改后的Employee
- class Employee implements Serializable{
- private String name1; //注意,这里略微改动一下数据域的名字
- Employee(String n){
- this.name1=n;
- }
- public String getName(){
- return this.name1;
- }
- }
- //测试,打印SUID=-2226350316230217613L
- long serialVersionUID=ObjectStreamClass.lookup(Class.forName("Employee")).getSerialVersionUID();
- System.out.println(serialVersionUID);
两次测试的SUID都不一样,不过你可以试试如果name域是static或transient声明的,那么改变这个域名是不会影响SUID的。
很显然,JVM正是通过检测新旧类SUID的不同,来检测出序列化对象与反序列化对象的不兼容。抛出java.io.InvalidClassException: Employee; local class incompatible:
很多时候,类定义的改变势在必行,但又不希望出现序列化的不兼容性。我们就可以通过在类中显示的定义serialVersionUID,并赋予一个明确的long值即可。这样会逃过JVM的默认兼容性检查。但是如果数据域名的改变会导致反序列化后,改变的数据域只能得到默认的null或者0或者false值。
6、 在上面第3点中谈到了一个不能成功序列化的Employee的列子,原因就是包含了一个不能序列化的ZipFile对象引用的数据域。但有时我们非常想将ZipFile所对应的本地文件路径进行序列化,是不是真的没有办法了呢?这里我们就将一个非常有用的应用。
当我们需要用writeObject(Object)方法对某个类对象序列化的时候,会首先对这个类对象的所有超类按照继承层次从高到低来写出每个超类的数据域。谁能保证每个超类都实现了Serializable接口呢? 其实,对于这些不能序列化的类,JVM会检查这些类是否有这样一个方法:
private void writeObject(ObjectOutputStream out)throws IOException
如果有,JVM会调用这个方法仍然对该类的数据域进行序列化。我们来看看JDK的ObjectOutputStream类中对这一部分的实现(我这里只列出了源码中的执行过程):
- //下面的方法从上到下进行调用
- writeObject(Object);
- //ObjectOutputStream的writeObject方法
- public final void writeObject(Object obj) throws IOException {
- writeObject0(obj, false);
- }
- //ObjectOutputStream, 底层写入Object的实现
- private void writeObject0(Object obj, boolean unshared) {
- if (obj instanceof Serializable) {
- writeOrdinaryObject(obj, desc, unshared);
- }
- //ObjectOutputStream
- private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) {
- writeSerialData(obj, desc);
- }
- //ObjectOutputStream, 对超类到子类的每个可序列化的类,写出数据域
- private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException{
- //如果类中有writeObject(ObjectOutputStream)方法,则通过底层进行调用
- if (slotDesc.hasWriteObjectMethod()) {
- slotDesc.invokeWriteObject(obj, this);
- }//如果没有此方法,则采用默认的写类数据域的方法。
- else {//这个方法会对可序列化的对象中的数据域进行写出,但是如果这个数据域是不可序列化而且没有writeObject(ObjectOutputStream)方法的类对象,那么将抛出异常。
- defaultWriteFields(obj, slotDesc);
- }
- }
ObjectOutputStream中的writeSerialData()方法说明了JVM检查writeObject(ObjectOutputStream out)这个私有方法的潜在执行机制。这就是说,我们可以通过构造这个方法,使得原本不能序列化的类的部分数据域可以序列化。下面我们就开始对ZipFile进行可序列化的改造吧!
- //自定义的一个可序列化的ZipFile,当然这个类不能继承JDK中的ZipFile,否则序列化将不可能完成。
- class SerializableZipFile implements Serializable{
- public ZipFile zf;
- //包含一个ZipFile对象
- SerializableZipFile(String filename) throws IOException{
- zf=new ZipFile(filename);
- }
- //对ZipFile中的文件名进行序列化,因为它是String类型的
- private void writeObject(ObjectOutputStream out)throws IOException{
- out.writeObject(zf.getName());
- }
- //对应的,反序列化过程中JVM也会检查类似的一个私有方法。
- private void readObject(ObjectInputStream in)throws IOException,ClassNotFoundException{
- String filename=(String)in.readObject();
- zf=new ZipFile(filename);
- }
- }
- //测试
- public static void main(String[] args) throws IOException, ClassNotFoundException{
- //序列化
- File file=new File("E:/aaa.txt");
- ObjectOutputStream oout=new ObjectOutputStream(new FileOutputStream(file));
- oout.writeObject(new SerializableZipFile("e:/aaa.zip"));
- oout.close();
- System.out.println("序列化成功");
- //反序列化
- ObjectInputStream oin=new ObjectInputStream(new FileInputStream(file));
- Object o=oin.readObject();
- oin.close();
- System.out.println("反序列化成功:"+((SerializableZipFile) o).zf.getName());
- }
- //序列化成功
- //反序列化成功:e:\aaa.zip
【总结】你所不知道的Java序列化的更多相关文章
- 你所不知道的java编程思想
读thinking in java这本书的时候,有这么一句话“在编译单元的内部,可以有一个公共(public)类,它必须拥有与文件相同的名字” 有以下疑问: 在一个类中说可以有一个public类,那是 ...
- 你所不知道的 Java 之 HashCode
之所以写HashCode,是因为平时我们总听到它.但你真的了解hashcode吗?它会在哪里使用?它应该怎样写? 相信阅读完本文,能让你看到不一样的hashcode. 使用hashcode的目的在于: ...
- 你所不知道的五件事情--java.util.concurrent(第二部分)
这是Ted Neward在IBM developerWorks中5 things系列文章中的一篇,仍然讲述了关于Java并发集合API的一些应用窍门,值得大家学习.(2010.06.17最后更新) 摘 ...
- 你所不知道的库存超限做法 服务器一般达到多少qps比较好[转] JAVA格物致知基础篇:你所不知道的返回码 深入了解EntityFramework Core 2.1延迟加载(Lazy Loading) EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public? 藏在正则表达式里的陷阱 两道面试题,带你解析Java类加载机制
你所不知道的库存超限做法 在互联网企业中,限购的做法,多种多样,有的别出心裁,有的因循守旧,但是种种做法皆想达到的目的,无外乎几种,商品卖的完,系统抗的住,库存不超限.虽然短短数语,却有着说不完,道不 ...
- Android中Context详解 ---- 你所不知道的Context
转自:http://blog.csdn.net/qinjuning/article/details/7310620Android中Context详解 ---- 你所不知道的Context 大家好, ...
- Android中Context详解 ---- 你所不知道的Context(转)
Android中Context详解 ---- 你所不知道的Context(转) 本文出处 :http://b ...
- 你所不知道的 URL
0.说明 第一幕 产品:大叔有用户反映账户不能绑定公众号.大叔:啊咧咧?怎么可能,我看看?大叔:恩?这也没问题啊,魏虾米.大叔:还是没问题啊,挖叉类.大叔:T T,话说产品姐姐是不是Java提供接口的 ...
- 你所不知道的C++
C++与C的不同 C++从诞生之初就号称和C是兼容的,正是这种兼容,使C++得以迅猛发展,然而也正是这种兼容,让C++背上了沉重的历史包袱.且不论其利弊,让我们来看看C++在兼容C的那部分中,与C语言 ...
- Android Context完全解析,你所不知道的Context的各种细节
Context相信所有的Android开发人员基本上每天都在接触,因为它太常见了.但是这并不代表Context没有什么东西好讲的,实际上Context有太多小的细节并不被大家所关注,那么今天我们就来学 ...
随机推荐
- 用MSBuild和Jenkins搭建持续集成环境(1)
http://www.infoq.com/cn/articles/MSBuild-1 你或其他人刚刚写完了一段代码,提交到项目的版本仓库里面.但等一下,如果新提交的代码把构建搞坏了怎么办?万一出现编 ...
- JAVA标签的使用跳出循环
public static void main(String args[]) { int i=10,j=10; outer: while (i > 0) { inner: while (j &g ...
- Java 7 中 NIO.2 的使用——第四节 文件和目录
Files类提供了很多方法用于检查在于你真正实际去操作一个文件或目录.这些方法强烈推荐,也非常有用,也能避免很多异常的发生.例如,一个很好的习惯就是在你试着移动一个文件从一个地方到另一个地方的时候,先 ...
- hdu 4000 Fruit Ninja 树状数组
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4000 Recently, dobby is addicted in the Fruit Ninja. ...
- 【BZOJ】【1011】【HNOI2008】遥远的行星
神奇的思路题QAQ 玛雅看到这题我就醉了,什么玩意……5%的误差?果断膜拜@ydc神犇的题解: 就是因为不清楚如何应用那个答案误差不超过5%啦. 从没见过这么诡异的题一下就懵了,问到了方法之后都还半信 ...
- Poj 1255 覆盖的面积 2014-07-28 12:29 116人阅读 评论(0) 收藏
覆盖的面积 Time Limit: 10000/5000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) Total Sub ...
- C# 模拟一个处理消息队列的线程类 Message Queue
// 模拟一个处理消息队列的类 class MessageHandler { // 消息队列 private Queue<string> messageQue = new Queue< ...
- MM1排队系统
#coding=utf-8 import time import random as rd #import math import pylab as pl def simulate(nameda,u) ...
- MEAN实践——LAMP的新时代替代方案(下)
在本系列文章的第一部分旨在介绍一些应用程序的基础技术细节和如何进行数据建模,而这个部分文章将着手建立验证应用程序行为的测试,并会指出如何启动和运行应用程序. 首先,编写测试 首先定义一些小型配置库.文 ...
- acdream1116 Gao the string!(扩展KMP)
今天是字符串填坑的一天,首先填的第一个坑是扩展KMP.总结一下KMP和扩展KMP的区别. 在这里s是主串,t是模式串. KMP可以求出的是以s[i]为结尾的串和 t前缀匹配的最长的长度.假如这个长度是 ...