天天看点

【笔记】Java序列化机制解析

  • 首先创建三个类,A、B和C,这三个类均有两个个int域,其中C还包含有一个A,代码如下所示,构造器及getter/setter省略,因为方法不参与序列化,因此以下讨论不包含方法。

public class A {     private int priA;     public int pubA; } public class B {     private int priB;     public int pubB; } public class C extends B{     private int priC;     public int pubC;     public A a; } public class Test {     public static void main(String[] args) throws  IOException, ClassNotFoundException {          String fileName = "serial.dat";          A a = new A(1, 2);          C c = new C(2, 3, 4, 5, a);//构造一个对象C          System.out.println("=======serialize  C=======");          FileOutputStream fou = new  FileOutputStream(fileName);          ObjectOutputStream objout = new  ObjectOutputStream(fou);          objout.writeObject(c);          objout.close();          System.out.println("========deserialize  C========");          FileInputStream fin = new  FileInputStream(fileName);          ObjectInputStream objin = new  ObjectInputStream(fin);          C nc = (C) objin.readObject();          System.out.println("========compare  c&nc=========");          System.out.println("c==nc: " + (c == nc));          System.out.println("c.equals(nc): " +  (c.equals(nc)));          System.out.println("original c: " +  c.toString());          System.out.println("nc: " + nc.toString());     } }

  • java.io.Serializable接口

  •     该接口是Java序列化的默认接口,该方法没有任何需要实现的抽象函数,仅仅是一个标记接口,代表当前类及其派生类支持序列化。并且当前类所有参与序列化的域都必须实现该接口。否则会报不支持序列化的错误
    • 【笔记】Java序列化机制解析
  • 然后讨论可序列化的数据
  1. 当前类中没有被transient修饰的数据都会参加序列化。
  2. 父类中的数据是否参加序列化视情况而定,父类也实现了Serializable接口时,父类的数据也会参与序列化
      • 【笔记】Java序列化机制解析
    •     当父类没有实现Serializable接口时,父类数据不参与序列化,并且必须拥有一个子类可访问的无参构造器(public或protected)。
      •     当缺少子类可见的无参构造器时会报没有有效的构造器的错误
        • 【笔记】Java序列化机制解析
      •     有子类可见的无参构造器时,当前类正常序列化,通过调用的父类默认构造器来初始化父类的域
        • 【笔记】Java序列化机制解析

小节

  1. 默认的序列化机制在恢复对象时直接使用二进制数据来生成一个新的对象,不调用当前对象的构造器。
  2. 从当前类一直到上溯到Object类,所有实现了Serializable接口的类的域都参与序列化,未实现Serializable接口的类需要提供一个子类可见的无参构造器,jvm使用该构造器来初始化未实现Serializable接口的类的域。

自定义序列化过程

  •     当需要对序列化过程进行控制,例如在序列化时写日志,对某些域不支持序列化,此时可以人为的将这些域的状态保存下来,然后在逆序列化时将其恢复。
  •     transient关键字:被该关键字标记的域会被自动序列机制忽略。
  •  private void writeObject(ObjectOutputStream out)  throws IOException和private void  readObject(ObjectInputStream in) throws  ClassNotFoundException, IOException方法
    •     这两个方法是匹配的,一般来说实现了一个就必须实现另一个,非transient域可以使用该方法按照自动序列化的方式进行序列化
    •     ObjectInputStream和ObjectOutputStream包含两个方法,in.defaultReadObject()和out.defaultWriteObject();这两个方法也是自动序列化机制默认调用的方法。
      •     defaultReadObject()方法从输入流读取数据,按照当前类的继承层次,从上到下的执行各个类的defaultReadObject(),以此恢复各个类的域(不直接对父类进行数据恢复)。
      •     defaultWriteObject()的执行顺序与defaultReadObject()相同,该方法只管当前类的域的序列化(不直接序列化父类的域)
    • 【笔记】Java序列化机制解析
  •     对于transient域的序列化需要手动进行
    •     transient域不参与对象序列化,在对象逆序列化时会被设为虚拟机的初始化值(0或null)。
      •     将A类的pubA标记为transient,执行结果如下
      • 【笔记】Java序列化机制解析
    • 对transient域的手动序列化通过直接调用输入输出流的相关写入写出方法来执行
      • 【笔记】Java序列化机制解析
      • 注意序列化和逆序列化的顺序是相同的。执行结果如下,此时手动控制了pubA的序列化和逆序列化。
      • 【笔记】Java序列化机制解析
  • private void readObjectNoData() throws ObjectStreamException;
    • 该方法也是序列化机制的一个方法,该方法不向数据流发送数据,也不从数据流中读入数据,该方法只负责在逆序列化之前对当前类的域设置初始化值。
    • 对一个类进行序列化,随后对该类进行拓展(extends),然后再对该类逆序列化,因为父类的数据不在序列化流中,因此无法对父类数据进行恢复,此时有以下三种情况:如果父类没有实现Serializable接口,那么使用无参构造器对父类数据进行初始化;如果父类实现了Serializable接口但是没有实现readObjectNoData()方法,那么使用虚拟机默认初始化值来初始化父类数据;如果父类同时实现了Serializable接口和readObjectNoData()方法,那么通过执行readObjectNoData()方法对父类数据进行初始化。
      • 【笔记】Java序列化机制解析
    • 通俗的来说,readObjectNoData()方法负责在逆序列化时对定义该方法的类进行默认初始化。
  • 两个不常用方法:ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException 和ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException
    • readResolve()方法在逆序列化完成后,重建对象返回前执行,即可以在逆序列化完成后通过该方法对对象进行某些操作,常见的用来保证单例和枚举类型(通过静态常量自定义的枚举类型)的唯一性,避免逆序列化导致出现多个单例对象。java语言的enum枚举类型可以保证序列化和逆序列化过程中的唯一性,不需要通过该方法保证
      • 【笔记】Java序列化机制解析
      • 【笔记】Java序列化机制解析
    • writeReplace()方法用来返回一个对象,该对象会替换掉要被序列化的对象,最终被写入到序列化流中
      • 【笔记】Java序列化机制解析
      • 【笔记】Java序列化机制解析
      • 【笔记】Java序列化机制解析
      • 在该测试中,writeReplace()方法创建了一个list对象,然后将对象a保存到list中,然后返回list,此时自动序列化机制会将list对象写入到输出流中,即执行list对象及其内部成员的序列化方法。
        • 注意,因为在序列化list时,其内部成员也需要list,因此不能将当前类的实例存在该方法返回的list中,否则会陷入无限循环导致程序溢出,即使不溢出也会导致程序出错。即list.add(this)是非法的,会导致this.writeReplace()方法不断被执行。
      • 此时被序列化的对象不再是C,虽然输出语句表明要序列化C,但是实际上被序列化的对象是writeReplace()方法返回的对象(在这里是包含a的list),因此通过ObjectInputStream流进行对象恢复时,读取到的也是list。
      • C类中没有任何方法可以解析writeReplace()方法返回的对象,比如这里是list,读回数据时,读到的也是list,List类的逆序列化方法会被执行,此时C类的所有逆序列化方法都不再起作用,读回方法如测试代码中红框所示
  • Externalizable接口
    • Externalizable接口提供了对序列化和逆序列化的完全控制,该接口是Serializable的子接口。
    • 该接口包含两个方法,分别用来读入和写入对象状态:
      • 【笔记】Java序列化机制解析
    • 基本规则:
      • 使用Externalizable时,序列化和逆序列化时类的UID必须相同,否则会报错。
      • 一旦使用了Externalizable接口,对象所有状态的序列化完全被该接口内的方法接管,自动序列化机制不再执行,即只执行当前类的Externalizable接口方法,当前类的所有域,无论是自己的还是从父类继承过来的,都需要在当前类内通过Externalizable接口方法进行序列化和逆序列化。当然通用做法应该是调用父类的Externalizable接口方法来序列化父类的域,除非父类没有实现Externalizable接口,此时需要当前类来对父类状态进行必要的序列化
        • 【笔记】Java序列化机制解析
  • 版本控制
    • 序列化对象时通过类的serialVersionUID来表明版本。
    • serialVersionUID是类的公共静态常量,如果被显式定义了的话,会使用定义的值;如果没有被定义,则虚拟机会根据Class文件来进行计算。
    • 当两个Class文件的serialVersionUID不同的话,虚拟机会拒绝执行逆序列化
      • 【笔记】Java序列化机制解析
    • 通过显式声明可以保证虚拟机不会拒绝逆序列化,此时虚拟机会“尽力”将数据匹配到执行逆序列化端的类文件
      • 修改方法和静态数据域不会对逆序列化产生影响,因为这两部分不参与序列化过程
      • 修改数据域在代码中的顺序不会对逆序列化过程产生影响。
      • 修改变量名或者新增变量名会对逆序列化产生影响,被修改或者新增的变量会被初始化为默认值
        • 【笔记】Java序列化机制解析
        • 修改了priC的名称后,可以看到逆序列化后的priC的数据丢失,说明逆序列化机制无法匹配对变量名称的修改。
      • 删除变量不会影响其余变量的逆序列化过程
      • 修改变量数据类型会对逆序列化过程产生影响,虚拟机不会自动执行对象的转换,默认两个同名变量不匹配,导致逆序列化失败
        • 【笔记】Java序列化机制解析
    • 总结:逆序列化时根据变量的名称进行匹配,当名称匹配类型不匹配时,逆序列化失败;当名称不匹配时,不执行该数据的逆序列化过程;对于当前类中新增的变量,由虚拟机初始化为默认值;对于序列化输入流中多出来的变量数据,因为没有变量匹配所以会丢弃。

继续阅读