Take a look at GW

【Java】详解java对象的序列化

目录结构:

contents structure [+]

1.序列化的含义和意义

序列化机制允许将实现序列化的Java对象转化为字节序列,这些字节序列可以保存到磁盘上,或通过网络传输,以备以后重新恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而单独存在。如果需要让某个类支持序列化机制,那么必须实现Serializable或Externalizable接口。在Java库中,已经有许多类实现了Serializable接口,Serializable是一个标记接口,该接口无需实现任何方法,它只是表明该类是可序列化的。所有可能在网络上传输的对象的类都应该是可序列化的,否则程序将会出现异常,比如RMI(Remote Method Invoke,远程方法调用)过程中的参数和返回值;所有需要保持到磁盘里的对象的类都必须是可序列化的。

使用java序列化的时候需要注意:
a.对象的类名,实例变量都会被序列化;方法,类变量,transient实例变量不会被序列化。
b.实现Serializable接口的类如果需要让某个实例变量不会序列化,则可在实例变量前加transient修饰符,而不是static关键字。虽然static关键字可以达到效果,但是static关键字不是这样用的。
c.保证序列化对象的实例变量也是可序列化的,否则需要使用transient关键字来修饰该变量。
d.反序列化对象时,必须有序列化对象的class文件。
e.通过网络、文件来传输序列化的对象时候,必须按照实际写入的顺序读取。

2.使用对象流实现序列化

在上面我们已经了解到,如果需要将某个对象保持到磁盘或通过网络传输,那么该类该类应该实现Serializable接口或Externalizable接口。
为了演示,首先定义一个Person类:

public class Person implements Serializable{
    public String name;
    public int age;
    public Person(){
        System.out.println("Person的无参构造器");
    }
    public Person(String name,int age)
    {
        this.name=name;
        this.age=age;
    }
    public String toString(){
        return "name:"+name+",age:"+age;
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        System.out.println("Person的clone方法里");
        return super.clone();
    }
}

下面的代码将Person对象保存到磁盘person.out文件中:

public class WriteObject {
    public static void main(String[] args) throws Exception{
        Person person=new Person("孙悟空",500);
        FileOutputStream fos=new FileOutputStream(new File("person.out"));
        ObjectOutputStream oos=new ObjectOutputStream(fos);
        oos.writeObject(person);
    }
}

接下来的代码将从person.out文件中,反序列化一个Person对象:

public class ReadObject {
    public static void main(String[] args) throws Exception{
        FileInputStream fis=new FileInputStream(new File("person.out"));
        ObjectInputStream ois=new ObjectInputStream(fis);
        Object obj=ois.readObject();
        System.out.println(obj.getClass().getSimpleName());
    }
}

可以看见控制台打印了

Person

观察上面反序列化和Person的代码。可以看出,通过实现Serializable接口序列化后,再反序列化重新构建对象是没有经过Person类的无参构造器和clone方法的。这里可以暂时理解为一种特殊创建对象的方式。

3.对象引用的序列化

在上面的案例中,我们定义的Person类有两个,name和age,分别为String和int类型。通过观察String可以发现String实现了Serializable接口。其实只要一个类里面有引用类型,那么这个引用类型也必须可序列化,否则拥有该类型成员变量的类也不能序列化。
例如,在下面定义了一个Teacher类,并且持有List<Person>的引用:

public class Teacher implements Serializable{
    public String name;
    public List<Person> students=null;
    public Teacher(String name,List<Person> students)
    {
        this.name=name;
        this.students=students;
    }
}

上面的类成员变量List<Person> students,其中List、Person和Person类中的引用类型成员变量都实现了Serializable接口,倘若有一个没有实现Serializable接口,那么这个Teacher都不能被成功序列化。
我们创建了三个对象students,teacher,teacher2:

        List<Person> students=new ArrayList<Person>();
        students.add(new Person("孙悟空",500));
        students.add(new Person("猪八戒",400));
        students.add(new Person("沙僧",300));
        
        Teacher teacher1=new Teacher("唐僧",students);//引用List<Person>
        Teacher teacher2=new Teacher("玄奘",students);//引用List<Person>

在这里需要注意的是,我们依次把这三个对象序列化到本地文件中,那么是否会得到三个不同的List<Person>对象呢,倘若是,那么teacher1和teacher2引用的对象就不是同一个对象了,显然违背了java序列化机制的初衷了。在java中,进行了特殊处理,同一个对象只会被序列化一次。
我们使用如下代码来进行验证:

public class WriteReadTest {
    public static void main(String[] args) throws Exception {
        List<Person> students=new ArrayList<Person>();
        students.add(new Person("孙悟空",500));
        students.add(new Person("猪八戒",400));
        students.add(new Person("沙僧",300));
        
        Teacher teacher1=new Teacher("唐僧",students);
        Teacher teacher2=new Teacher("玄奘",students);
        //开始序列化对象
        FileOutputStream fos=new FileOutputStream(new File("person.out"));
        ObjectOutputStream oos=new ObjectOutputStream(fos);
        oos.writeObject(students);
        oos.writeObject(teacher1);
        oos.writeObject(teacher2);
        //开始反序列化对象
        FileInputStream fis=new FileInputStream(new File("person.out"));
        ObjectInputStream ois=new ObjectInputStream(fis);
        List<Person> s=(List<Person>)ois.readObject();
        Teacher t1=(Teacher)ois.readObject();
        Teacher t2=(Teacher)ois.readObject();
        
        System.out.println(s==t1.students);//true
        System.out.println(s==t2.students);//true
        System.out.println(t1==t2);//false
    }
}

可以看出反序列化出来的三个List<Person>对象都是同一个对象。

java的序列化机制采用了如下的算法:

a.所有保存到磁盘中的对象都有一个序列化编号
b.当程序试图序列化一个对象时,程序将检查该对象是否被序列化过,只有该对象从未被序列化(在本次虚拟机中)过,系统才会将该对象转化为字节序列输出。
c.如果某个对象已经被序列化过了,程序只是输出一个序列化编号,而不是重新序列化该对象。


通过上面的算法,我们可以得出一个结论,倘若有一个对象被多次序列化,那么只有第一次会成功被序列化,其它几次只是输出序列化编号而已,例如:
可以用如下图来进行进一步理解:

4.自定义序列化

实现自定义序列化,可以通过实现Serializable或Externalizable接口。

4.1 采用实现Serializable接口实现序列化

通过实现Serializable接口来实现序列化是比较常用的,Serializable是一个标记性接口,实现该接口不需要做多余的额外工作。
接下来会介绍一些常见的操作和注意事项,
在一些特殊情况下,一个类里包含的实例变量是敏感信息,不希望被序列化到本地,那么可以在此变量上使用transient关键字。
例如:

public class Person implements Serializable{
    public transient String name;//transient表明 不允许被序列化到本地
    public int age;
    public Person(String name,int age)
    {
        this.name=name;
        this.age=age;
    }
}

测试:

        Person p1=new Person("孙悟空",500);
        //开始序列化对象
        FileOutputStream fos=new FileOutputStream(new File("person.out"));
        ObjectOutputStream oos=new ObjectOutputStream(fos);
        oos.writeObject(new Person("孙悟空",500));
        //开始反序列化对象
        FileInputStream fis=new FileInputStream(new File("person.out"));
        ObjectInputStream ois=new ObjectInputStream(fis);
        Person person1=(Person)ois.readObject();
        System.out.println("name:"+person1.name+",age:"+person1.age);//name:null,age:500

可以在需要被序列化的列中,定义writeReplace()方法。JVM在进行序列化时候,如果未定义该方法,则不会进行序列化,如果定义了该方法,那么该方法在writeObject之后调用。
writeObject方法的完整签名格式为:

Object writeReplace() throws ObjectStreamException

writeReplace在writeObject之后调用,一旦定义了writeReplace方法,那么由writeObject序列化的对象,会完全丢弃,程序被序列化的对象是writeReplace所返回的。
通过这个特点,我们可以把被序列化的对象,替换为我们想要的任意类型:
例如:

public class Person implements Serializable{
    public String name;
    public int age;
    public Person(String name,int age)
    {
        this.name=name;
        this.age=age;
    }
    Object writeReplace() throws ObjectStreamException
    {
        List<Object> list=new ArrayList<Object>();
        list.add(name);
        list.add(age);
        return list;//返回了一个List<Object>类型的数据
    }
}

我们看到writeReplace方法返回了一个List<Object>类型的数据。

    public static void main(String[] args) throws Exception {
        Person p1=new Person("孙悟空",500);
        //开始序列化对象
        FileOutputStream fos=new FileOutputStream(new File("person.out"));
        ObjectOutputStream oos=new ObjectOutputStream(fos);
        oos.writeObject(p1);
        //开始反序列化对象
        FileInputStream fis=new FileInputStream(new File("person.out"));
        ObjectInputStream ois=new ObjectInputStream(fis);
        List<Object> p=(List<Object>)ois.readObject();
        for(Object obj : p)
        {
            System.out.println(obj);
        }
    }

和writeReplace方法相对的就是readResolve方法,readResolve方法会在readObject()方法之后调用,
readResolve方法的完整签名:

Object readResolve() throws ObjectStreamException

由于readResolve方法会在readObject之后立即调用,该方法的返回值会替代原来反序列化的对象,而原来readObject反序列化的对象将会被立即抛弃。
readResolve方法在序列化单例类、枚举类时尤其有用。如果使用java5提供的枚举,当然没问题,但如果在java5以前,那么在序列化时,就需要注意了

public class Orientation implements Serializable{
    public static final Orientation HORIZONTAL=new Orientation(1);
    public static final Orientation VERTICAL=new Orientation(2);
    private int value;
    private Orientation(int value){
        this.value=value;
    }
}

这样的代码在java5以前经常用来表示枚举,如果使用如下的代码进行序列化:

       Orientation o1=Orientation.HORIZONTAL;
        //开始序列化对象
        FileOutputStream fos=new FileOutputStream(new File("person.out"));
        ObjectOutputStream oos=new ObjectOutputStream(fos);
        oos.writeObject(o1);
        //开始反序列化对象
        FileInputStream fis=new FileInputStream(new File("person.out"));
        ObjectInputStream ois=new ObjectInputStream(fis);
        Orientation o=(Orientation)ois.readObject();
        System.out.println(o==o1);//false

会发现结果为false,这显然不是我们想要的。这是因为反序列化的对象时重新构建的对象,我们可以定义readResolve方法来解决这个问题:

public class Orientation implements Serializable{
    public static final Orientation HORIZONTAL=new Orientation(1);
    public static final Orientation VERTICAL=new Orientation(2);
    private int value;
    private Orientation(int value){
        this.value=value;
    }
    Object readResolve() throws ObjectStreamException
    {
        if(value==1){
            return HORIZONTAL;
        }else if(value==2)
        {
            return VERTICAL;
        }else{
            return null;
        }
    }
}

这样一来,被序列化前后的对象就相等了。这样之所以,是因为序列化不包括静态变量,由于readObject后会立即调用readResolve方法,我们又在readResolve中返回了一个类变量,所以前后得到的是同一个对象。
接下来附张图片,表示writeObject,writeReplace,readObject,readResolve方法间的调用前后顺序:

4.2采用实现Externalizable接口实现序列化

采用实现Externalizable的方式和实现Serializable的方式具有相同的效果。在上面介绍Serializable里的常用操作和方法在Externalizable接口里也同样适用,这里就不一一介绍那些操作。
采用实现Externalizable接口的方法,必须重新Externalizable接口里的两个抽象方法writeExternal和readExternal方法。

public class Person implements Externalizable{
    public String name;
    public int age;
    public Person(){
        System.out.println("公共无参构造器");
    }
    public Person(String name,int age)
    {
        this.name=name;
        this.age=age;
    }
    /**
     * 在序列化的时候调用
     */
    @Override
    public void writeExternal(ObjectOutput out)
            throws IOException {
        out.writeObject(name);
        out.writeInt(age);
    }
    /**
     * 在反序列化的时候调用
     */
    @Override
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        this.name=(String)in.readObject();
        this.age=in.readInt();
    }
}

测试代码为:

    public static void main(String[] args) throws Exception {
        Person p=new Person("孙悟空",500);
        //开始序列化对象
        FileOutputStream fos=new FileOutputStream(new File("person.out"));
        ObjectOutputStream oos=new ObjectOutputStream(fos);
        oos.writeObject(p);
        //开始反序列化对象
        FileInputStream fis=new FileInputStream(new File("person.out"));
        ObjectInputStream ois=new ObjectInputStream(fis);
        Person p2=(Person)ois.readObject();
        System.out.println("name:"+p2.name+",age:"+p2.age);
    }

打印结果为:

公共无参构造器
name:孙悟空,age:500

我们可以看到执行了Person的公共无参构造器,这是使用Externalizable和Serializable的不同点;使用Externalizable来反序列化时,是调用反序列化的类的公共无参构造器,然后在readExternal方法中对成员变量赋值,而Serializable是不会调用任何构造器的。

5序列化的版本问题

通过前面的介绍,我们知道了反序列化java对象必须提供该对象的class文件,现在的问题是,顺着项目的升级,系统的class文件也会升级,java如何保证两个class文件的兼容性?
java序列化机制允许为序列化类提供一个private static final 的serialVersionUID值,该类变量的值用于表示该java类的序列化版本,也就是一个类升级后,只要它的serialVersionUID的值不变,序列化机制也会把他们当做同一个序列化版本。

public class Test{
    private static final long serialVersionUID=512L;
}

为了在反序列化时确保序列化版本的兼容性,最好在每个要序列化的的类中加入private static final long seriaVersionUID值的类变量,具体数值自己定义。这样,计时在某个类序列化之后,它所对应的类被修改了,该对象也依然可以被正确的反序列化。

可以通过JDK的bin目录下的serialver.exe工具来获得该类的serialVersionUID类变量的值。

serialver Person

 

posted @ 2017-05-11 00:09  HDWK  阅读(559)  评论(0编辑  收藏  举报