Java I/O 序列化机制

    对象序列化的目标是将对象保存到磁盘中,或者通过网络进行传输。序列化机制允许把内存中的Java对象转换成平台无关的二进制流,并在需要的时候恢复成原来的Java对象。序列化是保持对象轻量级持久的方式。为了让某个类是可序列化的,该类必须实现以下的两个接口之一:Serializable以及Externalizable。下面分别介绍这两个接口以及对象序列化的相关内容。

1.Serializable接口

    Java中很多类已经实现了Serializable接口,该接口仅仅是一个标记接口,实现该接口无须实现任何方法,它只表明该类的实例是可序列化的。在尝试序列化不支持 Serializable 接口的对象时,将抛出NotSerializableException。下面的代码演示了如何将对象序列化为二进制流以及如何从二进制流中反序列化从而恢复一个对象。

package io;
 
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
importjava.io.ObjectOutputStream;
import java.io.Serializable;
 
class Creature {
        Stringkind;
        Creature(){
               this("Mankind");
               System.out.println("InCreature Constructor");
        }
        Creature(Stringkind){
               this.kind= kind;
        }
}
 
class Man extends Creatureimplements Serializable
{
        /**
         * serialVersionUID:类的序列化版本号
         */
       
        privatestatic final long serialVersionUID = -8564385417353625750L;
        Stringname,gender;
        Man(Stringname,String gender){
               this.name= name;
               this.gender= gender;
               System.out.println("InMan Constructor");
        }
        publicString toString(){
               return"My name is "+name+",I am "+gender+" ,"+kind;
        }
        @Override
        publicboolean equals(Object o){
               if(o==null)return false;
               elseif(o instanceof Man){
                       Manman = (Man)o;
                       returnman.name.equals(name) &&
                                      man.gender.equals(gender);
               }
               elsereturn false;
        }
}
 
public class SerializeTest {
 
        publicstatic void writeMan(Man man){
               try{
                       //创建一个ObjectOutputStream输出流
                       ObjectOutputStreamoos = new ObjectOutputStream(
                               newFileOutputStream("man.txt"));
                       //将per对象写入输出流
                       oos.writeObject(man);
               }
               catch(IOException ex)
               {
                       ex.printStackTrace();
               }
        }
        publicstatic Man readMan(){
               Manman=null;
               try{
                       //创建一个ObjectInputStream输入流
                       ObjectInputStreamois = new ObjectInputStream(
                               newFileInputStream("man.txt"));
                       //从输入流中读取一个Java对象,读出来的为Object类型,需要将其强制类型转换为Man类
                       man= (Man)ois.readObject();
               }
               catch(Exception ex)
               {
                       ex.printStackTrace();
               }
               returnman;
        }
        publicstatic void main(String[] args) {
               //TODO Auto-generated method stub
               Manman = new Man("lcd", "male");
               writeMan(man);
               Manman1 = readMan();
               System.out.println(man1);//My name is lcd,I am male ,Mankind
               System.out.println(man.equals(man1));//true,两个对象的内容相同
               System.out.println(man==man1);//false,两个对象的引用不同,即不是同一个对象
               System.out.println(man.name==man1.name);//false     
        }
}

    上面的代码中,我们将Man类的对象man写入man.txt文件中,然后从文件流中恢复该对象。从最后的输出结果可以看出,恢复的对象与原来的对象各个字段的值是相等,但比较引用时,返回false,说明两个对象并不是同一个对象。除此之外,我们发现Man继承至Creature类,但Creature类并不是可序列化的,Java序列化机制对这种情况有如下规定:要允许不可序列化类的子类型序列化,可以假定该子类型负责保存和恢复超类型的公用(public)、受保护的 (protected) 和(如果可访问)包 (package) 字段的状态。仅在子类型扩展的类有一个可访问的无参数构造方法来初始化该类的状态时,才可以假定子类型有此职责。如果不是这种情况,则声明一个类为可序列化类是错误的。该错误将在运行时检测到。根据上面的规定,Creature类中必须有一个Man子类可访问的默认的无参数构造函数,在反序列化时,该Creature的构造器将会被调用来初始化类中的实例变量。在运行过程中,我们确实看到了反序列化过程中该方法被调用。若Creature中找不到对应的构造函数(包括将无参构造函数声明为private导致子类无法访问、或者没有默认的构造函数,只存在有参数的构造函数),反序列化过程将抛出异常。
    在序列化和反序列化过程中需要特殊处理的类必须使用下列准确签名来实现特殊方法:

    private voidwriteObject(java.io.ObjectOutputStream out) throws IOException;该方法负责写入特定类的对象的状态,以便相应的 readObject 方法可以恢复它。通过调用out.defaultWriteObject 可以调用保存 Object 的字段的默认机制。该方法本身不需要涉及属于其超类或子类的状态。通过使用 writeObject 方法或使用 DataOutput 支持的用于基本数据类型的方法将各个字段写入ObjectOutputStream,状态可以被保存。
     private void readObject(java.io.ObjectInputStreamin) throws IOException, ClassNotFoundException;该方法负责从流中读取并恢复类字段。它可以调用in.defaultReadObject 来调用默认机制,以恢复对象的非静态和非瞬态字段。该方法本身不需要涉及属于其超类或子类的状态。随后需要手动恢复不是采用默认机制保存的信息。
     private void readObjectNoData() throws ObjectStreamException;方法负责初始化特定类的对象状态。这在接收方使用的反序列化实例类的版本不同于发送方,并且接收者版本扩展的类不是发送者版本扩展的类时发生。在序列化流已经被篡改时也将发生;因此,不管源流是“敌意的”还是不完整的,readObjectNoData方法都可以用来正确地初始化反序列化的对象。

   ANY-ACCESS-MODIFIERObject writeReplace() throws ObjectStreamException;使用该准确的方法声明,如果此方法存在,在序列化时将被调用,将当前准备序列化的对象替换成该方法返回的对象,该对象必须是可序列化的!

    ANY-ACCESS-MODIFIER ObjectreadResolve() throws ObjectStreamException;实现此方法可以在从流中恢复对象时,使用该方法返回的指定对象来代替。该方法在序列化单例类、枚举类时尤其有用。

    以上各方法的调用顺序为:在序列化流时,首先调用ObjectOutputStream的writeObject方法来写入对象,系统调用该对象的writeReplace()方法[若该方法存在则调用,否则跳过此步],如果该方法返回另一个对象,系统将再次调用另一个对象的writeReplace()方法...直到方法不再返回另一个对象为止;之后,程序调用该对象自定义的writeObject()方法[若存在,则调用,否则调用默认的defaultWriteObject()方法]来保存对象的状态。在读出对象时,首先是调用流的readObject()方法,之后转到调用对象自定义的readObject()方法[若存在,则调用,否则为defaultReadObject],readResovle()方法会紧接着被调用[若存在],该方法的返回值将会代替原来反序列化的对象,原来的对象被丢弃。

2.Externalizable接口

    Java提供的另一个序列化的机制是实现Externalizable接口,该接口是继承自Serializable接口的。这种序列化方式完全由程序员决定存储和恢复对象数据。该接口有如下两个方法:

     voidreadExternal(ObjectInput in):对象实现 readExternal 方法来恢复其内容,它通过调用 DataInput 的方法来恢复其基础类型,调用 readObject 来恢复对象、字符串和数组。

     voidwriteExternal(ObjectOutput out):该对象可实现 writeExternal 方法来保存其内容,它可以通过调用 DataOutput 的方法来保存其基本值,或调用 ObjectOutput 的 writeObject 方法来保存对象、字符串和数组。

     这两个方法其实相当于对象中的readObject()方法和writeObject()方法,该接口同样可以使用writeReplace方法和readResovle()方法来选择替代对象。

3.序列化版本号

     Java序列化机制允许为序列化类提供一个“ANY-ACCESS-MODIFIERstatic final long serialVersionUID”值,该field用于标识该Java类的序列化版本号。该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致InvalidClassException。如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值。强烈建议 所有可序列化类都显式声明serialVersionUID 值,原因是计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,容易造成对象的反序列化因为版本不兼容而失败。若类中定义了serialVersionUID的值,则:

     (1)如果修改了类中的方法,或者静态的Field、瞬态Field,则反序列化不受任何影响,类定义中无须修改serialVersionUID的值;

     (2)修改了类中的非静态的Field、非瞬态Field,则可能导致序列化版本不兼容而失败,因此需要更新serialVersionUID 的值。如果仅仅是因为修改而包含了更多或者更少的Field,则仍然能够反序列化,这些多出来的Field值将被设置为默认值或者被忽略,否则序列化失败。

4.对象序列化要点总结

     (1)在一个Serializable对象进行还原的过程中,没有调用任何的构造器,包括默认的构造器。整个对象都是通过InputStream中取得数据恢复回来的。当这个Serializable类有多个父类时,包括直接父类和间接父类,这些父类要么有非private的无参数构造器(供反序列化时进行调用进行父类Field的初始化),要么必须也是可序列化的,否则在反序列化时将抛出InvalidClassException异常。如果父类是不可序列化的,只是带有无参数构造函数,则父类中定义的Field值不会序列化到二进制流中。而对于一个Externalizable对象而言,在反序列化时,会调用该对象的默认构造函数,该构造函数必须声明为public否则将抛出InvalidClassException异常。

     (2)所有保存到磁盘中的对象都有一个序列编号,当程序试图在某个流中序列化一个对象时,程序首先检查对象流中该对象是否已经被序列化过,只有该对象从未(在该对象流中)被序列化过,系统才会将该对象转换成字节序列并输出。如果该对象已经被序列化过,程序只是输出一个序列化编号,而不是重新序列化该对象。当序列化可变对象时,只有第一次调用writeObject方法来输出对象时才会将对象转换为字节序列,在后面的程序中,即使该对象的Field值发生了改变,再次调用writeObject方法输出对象,改变后的Field值也不会输出。除非在两次调用writeObject方法之间,调用了reset方法,该重置方法将丢弃已写入流中的所有对象的状态。重新设置状态,使其与新的ObjectOutputStream 相同。将流中的当前点标记为 reset,相应的 ObjectInputStream 也将在这一点重置。以前写入流中的对象不再被视为正位于流中。它们会再次被写入流。

     (3)对象的类名,Field(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、静态Field、transient字段都不会被序列化。如果一个可序列化的对象包含对某个不可序列化的对象的引用,那么整个序列化操作将会失败,并且会抛出一个NotSerializableException。

     (4)反序列化时,必须有序列化对象的class文件,否则抛出ClassNotFoundException。

     (5)当通过文件,网络来读取序列化后的对象时,必须按照实际写入的顺序读取。

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值