序列化和反序列化的学习

什么是序列化和反序列化

      序列化: 把对象转换为字节序列的过程
   反序列化:把字节序列恢复为对象的过程。

对象的序列化主要有两种用途

  1. 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;

  2. 在网络上传送对象的字节序列。

        在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。

  当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

序列化实现的方式

        如果需要将某个对象保存到磁盘上或者通过网络传输,那么这个类应该实现Serializable接口或者Externalizable接口之一。

        1.Serializable        

Serializable接口是一个标记接口,不用实现任何方法。一旦实现了此接口,该类的对象就是可序列化的。

  1. 创建一个用户类
public class User {
    private Integer id;

    private String username;

    private String password;

    public User() {
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

序列化

        序列化步骤:

  • 步骤一:创建一个ObjectOutputStream输出流;

  • 步骤二:调用ObjectOutputStream对象的writeObject输出可序列化对象

 public static void main(String[] args) throws IOException {

        User user = new User();
        user.setId(1);
        user.setPassword("123456");
        user.setUsername("zhangsan");
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dist\\user.text"));
        oos.writeObject(user);
    }

反序列化

  • 步骤一:创建一个ObjectInputStream输入流;

  • 步骤二:调用ObjectInputStream对象的readObject()得到序列化的对象。

    我们将上面序列化到uer.text的person对象反序列化回来

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("dist\\user.text"));
        User user = (User) ois.readObject();
        System.out.println(user);
    }

Serializable接口有何用?

上面在定义User类时,实现了一个Serializable接口,然而当我们点进Serializable接口内部查看,发现它竟然是一个空接口,并没有包含任何方法!

public interface Serializable {
}

试想,如果上面在定义Student类时忘了加implements Serializable时会发生什么呢?

实验结果是:此时的程序运行会报错,并抛出NotSerializableException异常:

 源码跟踪:ObjectOutputStreamwriteObject0()方法底层一看,才恍然大悟:

         如果一个对象既不是字符串数组枚举,而且也没有实现Serializable接口的话,在序列化时就会抛出NotSerializableException异常!

Serializable接口也仅仅只是做一个标记用!!!

        它告诉代码只要是实现了Serializable接口的类都是可以被序列化的!然而真正的序列化动作不需要靠它完成。

serialVersionUID号有何用?

private static final long serialVersionUID = 1217369220830889326L;

继续来做一个简单实验,还拿上面的User类为例,并没有人为在里面显式地声明一个serialVersionUID字段。

我们首先还是调用上面的serialize()方法,将一个User对象序列化到本地磁盘上的user.txt文件:

 public static void main(String[] args) throws IOException {

        User user = new User();
        user.setId(1);
        user.setPassword("123456");
        user.setUsername("zhangsan");
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dist\\user.txt"));
        oos.writeObject(user);
        oos.close();
    }

接下来我们在User类里面动点手脚,比如在里面再增加一个名为sex的字段,表示用户性别:

 实现反序列化:

 public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("dist\\user.txt"));
        User user = (User) ois.readObject();
        System.out.println(user);
        ois.close();
    }

运行报错:

这地方提示的信息非常明确了:序列化前后的serialVersionUID号码不兼容! 

从这地方最起码可以得出两个重要信息:

  • 1、serialVersionUID是序列化前后的唯一标识符
  • 2、默认如果没有人为显式定义过serialVersionUID,那编译器会为它自动声明一个!

两种特殊情况 

  • 1、凡是被static修饰的字段是不会被序列化的
  • 2、凡是被transient修饰符修饰的字段也是不会被序列化的

对于第一点,因为序列化保存的是对象的状态而非类的状态,所以会忽略static静态域也是理所应当的。

对于第二点,就需要了解一下transient修饰符的作用了。

如果在序列化某个类的对象时,就是不希望某个字段被序列化(比如这个字段存放的是隐私值,如:密码等),那这时就可以用transient修饰符来修饰该字段。

比如在之前定义的User类中,加入一个性别字段,但是不希望序列化到txt文本,则可以:

这样在序列化User类对象时,password字段会设置为默认值null,这一点可以从反序列化所得到的结果来看出: 

序列化的受控和加强

        自行编写readObject()函数,用于对象的反序列化构造,从而提供约束性。

        自行编写readObject()函数,那就可以做很多可控的事情:比如各种判断工作。

还以上面的User类为例,一般来说用户的年龄的应该在0 ~ 100之间,我们为了防止用户的的年龄在反序列化时被别人篡改成一个奇葩值,我们可以在用户(User)类中自行编写readObject()函数用于反序列化的控制:

private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException {

        // 调用默认的反序列化函数
        objectInputStream.defaultReadObject();

        // 手工检查反序列化后年龄的的有效性,若发现有问题,即终止操作!
        if( age<0 || age >100  ) {
            throw new IllegalArgumentException("年龄只能在0岁到100之间!");
        }
    }

比如我故意将用户年龄改外200,此时反序列化立马终止并且报错:

 为什么自定义的privatereadObject()方法可以被自动调用,这就需要你跟一下底层源码来一探究竟了

 又是反射机制在起作用!是的,在Java里,果然万物皆可“反射”(滑稽),即使是类中定义的private私有方法,也能被抠出来执行了。

单例模式增强

一个容易被忽略的问题是:可序列化的单例类有可能并不单例

举个代码小例子就清楚了。

懒汉模式

public class Singleton implements Serializable {

    /*构造函数私有化*/
    private Singleton(){
    }
    private static Singleton singleton = null;
    //构造实例化方法
    public static Singleton getSingleton(){
        if (singleton == null){
            singleton = new Singleton();
        }else {
            return singleton;
        }
        return singleton;
    }
}

 写一个验证主函数

public static void main(String[] args) throws IOException, ClassNotFoundException {

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dist/2.txt"));
        Singleton singleton = Singleton.getSingleton();
        oos.writeObject(singleton);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("dist\\2.txt"));
        Singleton singleton1 = (Singleton) ois.readObject();
        ois.close();

        //运行结果false
        System.out.println(singleton==singleton1);

    }

运行后我们发现:反序列化后的单例对象和原单例对象并不相等了。

解决办法是:在单例类中手写readResolve()函数,直接返回单例对象,来规避之:

 private Object readResolve() {
        return Singleton.singleton;
    }

 

这样一来,当反序列化从流中读取对象时,readResolve()会被调用,用其中返回的对象替代反序列化新建的对象。

2.Externalizable:强制自定义序列化

        通过实现Externalizable接口,必须实现writeExternal、readExternal方法。

public interface Externalizable extends java.io.Serializable {
 
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

 实体类,必须有无参构造函数

public class User1 implements Externalizable {
    private Integer id;

    private String username;

    private Integer age;

    private static String liang;

    public Integer getId() {
        return id;
    }

    public User1(Integer id, String username, Integer age) {
        this.id = id;
        this.username = username;
        this.age = age;
    }

    public User1() {
    }

    
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        StringBuffer stringBuffer = new StringBuffer(username);
        //字符串反转
        stringBuffer.reverse();
        out.writeObject(stringBuffer);
        out.writeInt(id);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.username  =((StringBuffer) in.readObject()).toString();
        this.id = in.readInt();
        this.age = in.readInt();
    }
}
如果没有无参构造函数,反序列化的时候会报以下错误

 序列化和反序列化

 public static void main(String[] args) throws IOException {
        User1 user = new User1(1,"aaa",100);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dist\\1.txt"));
        oos.writeObject(user);
        oos.close();
    }

public static void main(String[] args) throws IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("dist\\1.txt"));
    User1 user = (User1) ois.readObject();
    ois.close();
    System.out.println(user);
}

注意:Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供pulic的无参构造器,因为在反序列化的时候需要反射创建对象。

两种序列化对比

  1. 序列化内容:Externalizable自定义序列化可以控制序列化的过程和决定哪些属性不被序列化。
  2. Serializable序列化时不会调用默认的构造器,而Externalizable序列化时会调用默认构造器的
  3. 使用Externalizable时,必须按照写入时的确切顺序读取所有字段状态。否则会产生异常。例如,如果更改ExternalizableDemo类中的number和name属性的读取顺序,则将抛出java.io.EOFException。而Serializable接口没有这个要求。
  4. Externalizable性能略好,Serializable性能略差

虽然Externalizable接口带来了一定的性能提升,但变成复杂度也提高了,所以一般通过实现Serializable接口进行序列化。

源码导读

        建议看下这个博客

Serializable序列化与反序列化原理 - 掘金深入序列化源码,找到问题答案!1. 序列化为什么要实现Serializable?2. 序列化为什么要重写serialVersionUID和它的作用是什么?......https://juejin.cn/post/7013561917205643301#heading-0

以上内容属于个人笔记整理,如有错误,欢迎批评指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值