我们都知道,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序列化的更多相关文章

  1. 你所不知道的java编程思想

    读thinking in java这本书的时候,有这么一句话“在编译单元的内部,可以有一个公共(public)类,它必须拥有与文件相同的名字” 有以下疑问: 在一个类中说可以有一个public类,那是 ...

  2. 你所不知道的 Java 之 HashCode

    之所以写HashCode,是因为平时我们总听到它.但你真的了解hashcode吗?它会在哪里使用?它应该怎样写? 相信阅读完本文,能让你看到不一样的hashcode. 使用hashcode的目的在于: ...

  3. 你所不知道的五件事情--java.util.concurrent(第二部分)

    这是Ted Neward在IBM developerWorks中5 things系列文章中的一篇,仍然讲述了关于Java并发集合API的一些应用窍门,值得大家学习.(2010.06.17最后更新) 摘 ...

  4. 你所不知道的库存超限做法 服务器一般达到多少qps比较好[转] JAVA格物致知基础篇:你所不知道的返回码 深入了解EntityFramework Core 2.1延迟加载(Lazy Loading) EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public? 藏在正则表达式里的陷阱 两道面试题,带你解析Java类加载机制

    你所不知道的库存超限做法 在互联网企业中,限购的做法,多种多样,有的别出心裁,有的因循守旧,但是种种做法皆想达到的目的,无外乎几种,商品卖的完,系统抗的住,库存不超限.虽然短短数语,却有着说不完,道不 ...

  5. Android中Context详解 ---- 你所不知道的Context

    转自:http://blog.csdn.net/qinjuning/article/details/7310620Android中Context详解 ---- 你所不知道的Context 大家好,  ...

  6. Android中Context详解 ---- 你所不知道的Context(转)

    Android中Context详解 ---- 你所不知道的Context(转)                                               本文出处 :http://b ...

  7. 你所不知道的 URL

    0.说明 第一幕 产品:大叔有用户反映账户不能绑定公众号.大叔:啊咧咧?怎么可能,我看看?大叔:恩?这也没问题啊,魏虾米.大叔:还是没问题啊,挖叉类.大叔:T T,话说产品姐姐是不是Java提供接口的 ...

  8. 你所不知道的C++

    C++与C的不同 C++从诞生之初就号称和C是兼容的,正是这种兼容,使C++得以迅猛发展,然而也正是这种兼容,让C++背上了沉重的历史包袱.且不论其利弊,让我们来看看C++在兼容C的那部分中,与C语言 ...

  9. Android Context完全解析,你所不知道的Context的各种细节

    Context相信所有的Android开发人员基本上每天都在接触,因为它太常见了.但是这并不代表Context没有什么东西好讲的,实际上Context有太多小的细节并不被大家所关注,那么今天我们就来学 ...

随机推荐

  1. 软件工程课堂练习——N层电梯只停一层求乘客爬楼层数最少(基本方法+优化方法)

    题目: •石家庄铁道大学基础大楼一共有四部电梯,每层都有人上下,电梯在每层都停.信1201-1班的张一东觉得在每层都停觉得不耐烦. •由于楼层不太高,在上下课高峰期时时,电梯从一层上行,但只允许停在某 ...

  2. kernel nf_conntrack: table full, dropping packet[转载]

    http://blog.yorkgu.me/2012/02/09/kernel-nf_conntrack-table-full-dropping-packet/ 综合:ip_conntrack就是li ...

  3. Weblogic环境下hibernate、antlr类加载冲突问题分析及解决方案

    公司应用项目在客户部署时经常遇到此类问题,为避免实施部署时增加配置量,花了点时间找到了此问题的终极解决办法(方案二.修改org.hibernate.hql.ast.HqlLexer的源代码).在此进行 ...

  4. Python python 基本语法

    程序1 def buildConnectionString(params): """Build a connection string from a dictionary ...

  5. 1.C#基础篇-->封装、继承和多态

    面向对象三要素:封装.继承和多态.正确理解这三个要素,才能在编程中建立面向对象的思想. 1.封装使用篇 作用:好的封装增加代码的可读性,易于维护. 什么情况下使用封装,封装的原则是? 1>功能相 ...

  6. 【BZOJ】【1150】【CTSC2007】数据备份Backup

    堆/贪心 一共N-1个元素……用堆维护最大值,取了第x个元素以后,插入v[x-1]+v[x+1]-v[x]这个元素,如果再取这个新元素就表示不取x,而取x-1和x+1……大概就是这种“带反悔”的思路吧 ...

  7. POI中设置Excel单元格格式

    引用:http://apps.hi.baidu.com/share/detail/17249059 POI中可能会用到一些需要设置EXCEL单元格格式的操作小结: 先获取工作薄对象: HSSFWork ...

  8. 父页面 调用iframe方法

      父页面调用Iframe的方法 document.getElementById("tabIf0").contentWindow.Search();     Jquery 方式: ...

  9. 寒假222_codeforces 290 div 2 D

    序号5: 想了很久的DP ,应该很简单,但是.. 题目直接转化为求n个数中选一些数GCD=1且花费最小 数比较大 map  HASH 还有一点 我们知道 GCD(X,X*Y)==X; 所以我的代码里不 ...

  10. mongodb 主从服务器

    @set mongod=..\bin\mongod.exe set keyFile=key.key if not exist %keyFile% ( echo 123456>%keyFile% ...