java I/O系统(9)-对象序列化与还原

来源:互联网 发布:is网络同步工具 编辑:程序博客网 时间:2024/04/30 21:32

引言

万物皆对象,在我们程序运行中,对象只要在引用链上存在引用,那么它就会一直存在。但是当我们程序结束的时候,那么对象就会消亡。那么在jvm不运行的时候我们仍能够保存下来是非常有意义的,在java中可以用序列化来实现。序列化其实也是IO系统中的一部分。在本篇博文中,详细介绍对象序列化的概念,不同序列化的方式和结果,并给出相应的demo。注意本文所说的序列化包括序列化与反序列化。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290

序列化概念

序列化是指把对象通过IO系统转化成字节从而存储在磁盘当中,需要的时候可以还原成对象。也就是对象持久化的一个方式。

序列化意义

对象的生命周期是随着引用存在的,如果引用失效或者程序结束,那么这个对象就不复存在。但是我们可以通过序列化的方式把对象转成字节流从而存储在磁盘当中,在任何我们需要的时候再从新恢复成一个完整的对象。换句话说,就是对象持久化存储。
在我们网络通信的过程中,我们可以把对象序列化之后把它放入网络通信当中,这就弥补了不同操作系统之间的差异。不管你是从什么机器上序列化产生的字节流,在任意存在jvm虚拟机的情况下都可以从新转化成对象。

序列化设计IO

在序列化的过程中,根本上其实是一套IO操作,这个IO操作主要针对的就是对象。在IO系统中我们用ObjectInputStream与ObjectOutputStream来实现。在这两个对象流中存在两个方法readObejct和writeObject来实现对象的序列化输出和反序列化写入。

序列化方式

序列化是通过IO流来实现的,但是如果要序列化某一个对象,那么这个对象必须要实现了Serializable或Externalizable接口,否则在IO流操作的时候会抛出异常。

也就是说序列化对象存在两种方式:①Serializable;②Externalizable。那么这两个到底有什么区别呢?对于前者来说是对象的全自动序列化,它对对象中的所有的属性都会序列化,除却transient关键字标记的属性。对于后者来说,必须自己控制序列化过程,也就是说必须实现readExternal方法与writeExternal方法来控制序列化进行,同时后者反序列化的过程中,必须要执行对象的默认构造函数,所以说如果对象不存在可以调用的默认构造函数,那么就会抛错。

后面会详细介绍两者序列化的不同。

对象网

对象网是指序列化对象中对象之间引用的关系。比如说我序列化了A对象,A对象引用了B对象,B对象引用了C对象。那么在序列化之后这个对象之间的关系也是一同会写入字节流中进行持久化。在我们反序列化的过程中,能完整的还原出他们对象之间的关系。

transient关键字

transient是java的关键字,它表示在对象序列化的过程中,我们可以标记某一个敏感的属性,要求它在序列化的过程中唯独对这个属性不序列化,也就是说不把这个敏感属性持久化。
比如说在用户系统当中,我们需要对用户进行序列化存储后进行通信,但是我们不希望暴露这个用户的密码属性,那么我们就可以对密码这个属性进行transient关键字标记:

 private transient String password;

Serializable序列化

在此处的代码需要体现出以下关键点:①对象的序列化和反序列化;②transient标记属性不会序列化;③能体现出对象的对象网关系;④反序列化不需要调用任何构造函数

假设存在一个用户体系(user类),他们有自己各自的兴趣(interest类)。在用户体系中我们不希望在序列化的过程中暴露password字段,所以我们用transient标记它。接着我们对着两个类的构造函数选择包级别私有,如果反序列化需要调用构造函数那么就会抛错。同时在两个类中我们都重写toString方法,在测试的时候方便打印出完整的信息。下面就是这两个类:

package com.brickworkers.io;import java.io.Serializable;public class User implements Serializable{    private static final long serialVersionUID = 3667335206886584270L;    private String name;    private String phone;    private transient String password;    private Interest interest;    //无参构造函数    User() {        name = "brickworker";        phone = "157110";        password = "123456";    }    User(String name, String phone, String password){        this.name = name;        this.password = password;        this.phone = phone;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public String getPhone() {        return phone;    }    public void setPhone(String phone) {        this.phone = phone;    }    public String getPassword() {        return password;    }    public void setPassword(String password) {        this.password = password;    }    public Interest getInterest() {        return interest;    }    public void setInterest(Interest interest) {        this.interest = interest;    }    @Override    public String toString() {        return "name:" + name + "  phone:"+ phone + "  password:" + password+ "  "+ interest;    }}
package com.brickworkers.io;import java.io.Serializable;public class Interest implements Serializable{    private static final long serialVersionUID = -3147319655720895848L;    private String name;    private String description;    Interest(String name, String description) {        this.name = name;        this.description = description;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public String getDescription() {        return description;    }    public void setDescription(String description) {        this.description = description;    }    @Override    public String toString() {        return "name:" + name+" description:" + description;    }}

接下来我们测试序列化和反序列化结果:

package com.brickworkers.io;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;public class SerializeTest {    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {        //定义一个User类和Interest类对象        User user = new User("brickworker", "110", "123456");        Interest interest = new Interest("爬山", "爬山有益身心健康");        user.setInterest(interest);        //打印对象初始状态        System.out.println("对象初始状态:");        //重写了toString方法,可以直接打印对象        System.out.println(user);        //输出流,把对象通过序列化到磁盘        //try-with-source,会自动关闭流        try(ObjectOutputStream ops = new ObjectOutputStream(new FileOutputStream("F:/java/io/user.out"))){            ops.writeObject(user);        }        //持久化结束之后,再进行反序列化,把对象恢复        //try-with-source,会自动关闭流        try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream("F:/java/io/user.out"))){            User readUser = (User)ois.readObject();            //打印反序列化的对象结果            System.out.println("反序列化之后的结果");            System.out.println(readUser);        }    }}//输出结果://对象初始状态://name:brickworker  phone:110  password:123456  name:爬山 description:爬山有益身心健康//反序列化之后的结果//name:brickworker  phone:110  password:null  name:爬山 description:爬山有益身心健康////

通过上面的代码,我们可以看到①对象序列化和反序列化之后的结果是有所区别的,因为password是transient的,所以序列化的时候这个属性就被屏蔽了,并不会持久化到磁盘。②对象网还是存在,两个对象的嵌套方式也得以还原。③对象还原的时候并不需要调用构造函数。

在这里值得一提的是,如果仅仅user类实现了Serializable接口是不够的,如果序列化的过程中存在对象网,那么它所关联的对象也必须要实现序列化,也就是说Interest也必须实现Serializable接口。还有一点,如果是继承了父类,而父类是实现了Serializable接口的,那么子类也默认实现该接口。

Serializable序列化方式如何控制序列化

在前面的代码中提过一种控制方式了,就是用transient关键字标记的属性不会被序列化。但是Serializable序列化方式还存在着别的控制方式。那就是直接在要序列化的对象中写两个方法(writeObject和readObject)。注意,这个不是重写父类方法,只是Serializable序列化在IO流处理的时候,如果被序列化对象中本身就存在这两个方法就优先调用它。

我们在User类中写入writeObject和readObject方法,测试类还是原先的测试类:

package com.brickworkers.io;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.Serializable;import java.util.stream.Stream;public class User implements Serializable{    private static final long serialVersionUID = 3667335206886584270L;    private String name;    private String phone;    private transient String password;    private Interest interest;    //无参构造函数    User() {        name = "brickworker";        phone = "157110";        password = "123456";    }    User(String name, String phone, String password){        this.name = name;        this.password = password;        this.phone = phone;    }    private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{        name = ois.readUTF();    }    private void writeObject(ObjectOutputStream ops) throws IOException{        ops.writeUTF(name);    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public String getPhone() {        return phone;    }    public void setPhone(String phone) {        this.phone = phone;    }    public String getPassword() {        return password;    }    public void setPassword(String password) {        this.password = password;    }    public Interest getInterest() {        return interest;    }    public void setInterest(Interest interest) {        this.interest = interest;    }    @Override    public String toString() {        return "name:" + name + "  phone:"+ phone + "  password:" + password + " interest:" + interest;    }}

在这个类中我们加入了readObject和writeObject两个独立方法,在write方法中我们只序列化了name这一个熟悉,在readObject的时候也只处理这么一个属性。在测试直接用上面的测试例子,一个代码都不用改,大家可以看看测试结果:

//对象初始状态://name:brickworker  phone:110  password:123456 interest:name:爬山 description:爬山有益身心健康//反序列化之后的结果//name:brickworker  phone:null  password:null interest:null////

可以在序列化的过程中只有name这么一个字段被序列化了,其他的字段都没有被序列化。这就是在实现Serializable接口第二种控制序列化的方法。

值得一说的是,看客们不用去纠结为什么会执行User类中的readObejct和writeObject方法,你只要知道在序列化过程中,在实现Serializable接口的情况下,IO操作会先判断对象中有没有这两个方法,如果有就优先使用这两个方法,同时如果你要实现它原本的序列化只需要执行default方法就行,像下面这样:

    private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{        ois.defaultReadObject();    }    private void writeObject(ObjectOutputStream ops) throws IOException{        ops.defaultWriteObject();    }

Externalizable序列化

接下来我们说一说Externalizable序列化。实现Externalizable接口的序列化方式天然就需要自己控制序列化的属性,在实现这个接口的时候必须要实现两个方法:

    @Override    public void writeExternal(ObjectOutput out) throws IOException {        // TODO Auto-generated method stub    }    @Override    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {        // TODO Auto-generated method stub    }

这个其实和我们前面说的Serializable接口序列化控制的第二种方法很像,需要在需要序列化对象中添加两个方法,通过这2个方法对序列化对象的属性控制。我们改写User类,使它实现Externalizable接口,并书写如上方法。在这个例子中,我们需要实现以下这些目标:①对象成功序列化和反序列化;②transient关键字修饰的属性是否被序列化。③能体现出对象的对象网关系;④反序列化必须要调用默认构造函数。

修改之后的User类:

package com.brickworkers.io;import java.io.Externalizable;import java.io.IOException;import java.io.ObjectInput;import java.io.ObjectInputStream;import java.io.ObjectOutput;import java.io.ObjectOutputStream;import java.io.Serializable;import java.util.stream.Stream;public class User implements Externalizable{    private String name;    private String phone;    private transient String password;    private Interest interest;    //无参构造函数    User() {        name = "brickworker";        phone = "157110";        password = "123456";    }    User(String name, String phone, String password){        this.name = name;        this.password = password;        this.phone = phone;    }    @Override    public void writeExternal(ObjectOutput out) throws IOException {        out.writeUTF(name);        out.writeUTF(phone);        out.writeUTF(password);        out.writeObject(interest);    }    @Override    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {        name = in.readUTF();        phone = in.readUTF();        password = in.readUTF();        interest = (Interest) in.readObject();    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public String getPhone() {        return phone;    }    public void setPhone(String phone) {        this.phone = phone;    }    public String getPassword() {        return password;    }    public void setPassword(String password) {        this.password = password;    }    public Interest getInterest() {        return interest;    }    public void setInterest(Interest interest) {        this.interest = interest;    }    @Override    public String toString() {        return "name:" + name + "  phone:"+ phone + "  password:" + password + " interest:" + interest;    }}

修改之后的Interest类:

package com.brickworkers.io;import java.io.Externalizable;import java.io.IOException;import java.io.ObjectInput;import java.io.ObjectOutput;public class Interest implements Externalizable{    private String name;    private String description;    Interest(String name, String description) {        this.name = name;        this.description = description;    }    public Interest() {        name = "爬山";        description = "爬山有益身心健康";    }    @Override    public void writeExternal(ObjectOutput out) throws IOException {        out.writeUTF(name);        out.writeUTF(description);    }    @Override    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {        name = in.readUTF();        description = in.readUTF();    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public String getDescription() {        return description;    }    public void setDescription(String description) {        this.description = description;    }    @Override    public String toString() {        return "name:" + name+" description:" + description;    }}

测试类不用修改,直接进行测试,你会发现抛错了:
Exception in thread “main” java.io.InvalidClassException: com.brickworkers.io.User; no valid constructor
这个错误告诉你在User对象中没有默认的构造器,仔细观察上面的代码,你会发现两个构造器我都是用default来实现的,所以是不可访问的构造器,所以我们先要把user类和interest类的无参构造器设置成public。
在这里需要注意以下几点:①序列化对象必须要有可调用的显式无参构造器或者默认构造器;②序列化对象的参数构造器无影响;③对象网中的其他对象也必须要有显式无参构造器或者默认构造器

把User类中的无参构造器换成public就可以顺利进行测试,测试结果如下:

//对象初始状态://name:brickworker  phone:110  password:123456 interest:name:爬山 description:爬山有益身心健康//反序列化之后的结果//name:brickworker  phone:110  password:123456 interest:name:爬山 description:爬山有益身心健康////

从测试结果我们可以看出,①序列化和反序列化都是成功的,对象成功还原;②transient关键字修饰的对象也被正常序列化;③有完整的对象网

所以,transient关键字只有在Serializable默认的自动序列化中才会生效。

值得注意的是,在IO系统中writeXXX和readXXX是有顺序可言的,比如说在Interest类中两个方法是如下这么实现的:

    @Override    public void writeExternal(ObjectOutput out) throws IOException {        out.writeUTF(name);        out.writeUTF(description);    }    @Override    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {        description = in.readUTF();         name = in.readUTF();    }

先写入name,再写入description,但是在读取的时候先读取description,再读取name。这是错误的,因为写入和读取时有顺序的,读取必须要按照写入的顺序读写,不然结果就会是错误的,务必谨记!

如果对导出的user.out的内容有兴趣的,可以下载一个winhex软件,它是一个二进制码文。

关于实现Serializable接口之后设立的serialVersionUID,它其实对版本进行控制,通过比较serialVersionUID可以确定是否可以成功的反序列化。这一块内容篇幅问题不展开讨论,有兴趣的可以自己探究。

好了,基本上已经把我自己知道的所有序列化都写完了,最后,我再说一点有意思的东西,在存在对象网的序列化中,支持两种序列化方式混合使用,也就是说User类你可以实现Externalizable接口,但是Interest可以实现Serializable接口。希望对你下次面试有所帮助。

原创粉丝点击