Java序列化——Serializable、Externalizable源码阅读与总结

一、Java序列化概述

1、什么是序列化

序列化(Serialization):是将对象的状态信息转换为可以存储或传输的形式的过程。更通俗地讲,是将该对象字段和状态信息以字节流的方式输出到目的地。

2、序列化的应用场景

  • 一是实现pojo对象的读写操作,将每个对象转换为字节流,而这些字节流可以被持久化到设备上,再次读取时会将字节流还原成对象。当我们希望某些数据能在程序停止运行后,还能继续存在。在程序再次执行时还能获取这些数据时,或者让其他的程序也能够利用这些数据资源时。这就是我理解的应用场景一。
  • 二是实现网络间的数据传输。网络间的数据传输是高频发而且数据量也是非常大的。以订单数据传输为例,当我们希望获取到订单类里的全部数据并据此生成一份订单的excel文件时,这个订单类就必须要实现序列化,这是我理解的应用场景二。

3、Java序列化的方式

Java序列化提供两种方式。

  • 一种是实现Serializable接口
    使用该方式非常简单,通过重写该接口预置的5个方法,可以达到对序列化的控制。不重写这些方法,也可以自动序列化,非常方便。
  • 另一种是实现Externalizable接口。
    不像Serializable接口只是一个标记接口,Externalizable接口强制你自己动手实现串行化和反串行化方法。它的效率比Serializable高一些,并且可以决定哪些属性需要序列化(即使是transient修饰的),但是要求必须重写两个方法。Externalizable对小数目对象有效的多。但是对大量对象,或者重复对象,则效率低。

4、Java序列化的特点

java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的”深复制”,即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。

二、Java序列化方式

Serializable接口

java.io.Serializable的源码注释大概分为以下几个点

  • java.io.Serializable是一个空接口:
public interface Serializable {
}

源码注释中说到:

* Serializability of a class is enabled by the class implementing the
 * java.io.Serializable interface. Classes that do not implement this
 * interface will not have any of their state serialized or
 * deserialized. 

大意是说,只有实现该接口的类,才能序列化和反序列化(注意,Externalizable也继承了该接口)。

  • 为了允许非序列化类的子类型进行序列化(例如,子类实现了Serializable接口,但是父类并没有实现),子类型需要承担父类的public、protected和package访问权限字段的存储和恢复任务。因此,需要父类提供一个无参的构造器,以便子类进行状态的初始化。
  • 在反序列化期间,非序列化类的字段将会被public或者protected类型的无参构造器初始化。父类的构造器必须要能够被可序列化的子类所访问。可序列化子类的字段将会被流恢复。
  • 当遍历一个图,可能会遇到一个不支持序列化接口的对象。在这种情况下,将会抛出NotSerializableException并标志该类为不可序列化。
  • 一些在序列化或者反序列化期间需要做一些特别操作的类,必须实现下面这些完整签名的方法:
private void writeObject(java.io.ObjectOutputStream out) throws IOException
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
  • writeObject()方法用于将对象的当前状态写入到一个对象输出流中,并由输出流决定余下的操作(如,是输出到本地文件还是网络中)
  • readObject()负责从指定对象输入流中读出并恢复类的字段。该方法可能会使用in.defaultReadObject来调用默认逻辑恢复对象的非static和非transient字段。
  • readObjectNoData()方法用于一个并不常见的情景:序列化者(写者)并没有父类,但是反序列化者(读者)有父类,在这种情况下,从输入流中不能得到父类的信息。此时,readObjectNoData()方法将会调用父类的构造器创建一个新的父类,使得反序列化得以进行。
  • 如果一个类在序列化时,需要指定一个替代对象写入流中,他应该实现下面这个精确定义的方法:
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
  • 保护性恢复对象(同时也可以替换对象)——readResolve
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

readResolve会在readObject调用之后自动调用,它最主要的目的就是让恢复的对象变个样,比如readObject已经反序列化好了一个Person对象,那么就可以在readResolve里再对该对象进行一定的修改,而最终修改后的结果将作为ObjectInputStream的readObject的返回结果;其最重要的应用就是保护性恢复自己手动实现的单例、枚举类型的对象(Java 5之后的版本都实现了enum类型的自动保护性恢复,但是Java5之前的老版本还是不行!)

serialVersionUID

serialVersionUID是Java序列化中用来验证版本一致性的标志。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)
一个可序列化的类可以通过设置一个名为serialVersionUID的字段来显示设置自己的serialVersionUID。
这个字段必须严格遵循以下格式:

ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;

如果一个类没有显示声明自己的serialVersionUID,那么运行时序列化系统会根据类的当前状态生成 一个64位的hash码作为默认的serialVersionUID。
然而,所有的可序列化类都被强烈建议显示声明自己的serialVersionUID,因为默认的serialVersionUID对那些编译器相关的细节高度敏感,这可能会导致在反序列化时产生意外的InvalidClassException。
因此,为了保证在不同编译环境下serialVersionUID的一致性,一个可序列化类必须显示声明自己的serialVersionUID值。同时,也强烈建议将serialVersionUID设置为private访问权限,这样,这个serialVersionUID就是类型唯一绑定的了,即使它的子类也不会使用该字段。
数组(Array)类不能显示设置serialVersionUID,因此它们总是使用默认的serialVersionUID。但是数组类并不依赖serialVersionUID进行匹配。

Externalizable

Externalizable源码

public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
  • 实现了该接口的类,需要自己承担保存和恢复实例内容的责任。子类通过实现writeExternal和readExternal方法来完善对序列化和反序列化内容和格式的控制。这些方法必须显式地与父类型协调以保存其状态。
  • 这两个方法取代了Serializable中的writeObject和readObject方法。对象序列化机制使用Serializable和Externalizable接口。对象持久化机制也可以使用它们。
  • 在实现了Externalizable接口的类中,每一个被存储的对象都要经过Externalizable接口的测试。如果这个对象支持Externalizable,那么该接口的writeExternal将会被调用。如果这个对象不支持writeExternal但是实现了Serializable接口,那么这个对象将会通过ObjectOutputStream进行存储。
  • 当一个实现了Externalizable接口的对象被反序列化时,会首先调用该类的无参构造器,然后调用readExternal方法。而实现了Serializable接口的对象则是通过ObjectInputStream来读入对象的。
  • 一个Externalizable类型的实例可以通过writeReplace()方法和readResolve()来指定在实例化过程中的替代对象。
writeExternal(ObjectOutput out)方法
void writeExternal(ObjectOutput out) throws IOException;

子类需要实现writeExternal方法来保存其内容,该方法是通过调用其原始值的DataOutput方法,或者调用对象、字符串和数组的ObjectOutput的writeObject方法。

readExternal(ObjectInput in)方法
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

该方法通过调用原始类型的DataInput方法,或者对象、字符串、数组等引用类型的readObject方法来读入字节流,并恢复对象的状态。readExternal方法必须按照与writeExternal方法相同的顺序和类型来读取数据。

JDK类库中序列化与反序列化步骤

序列化

步骤一:创建一个对象输出流,它可以包装一个其它类型的目标输出流,如文件输出流:

ObjectOutputStream out = new ObjectOutputStream(new fileOutputStream(“D:\\objectfile.obj”));

步骤二:通过对象输出流的writeObject()方法写对象:

out.writeObject(“Hello”);
out.writeObject(new Date());

反序列化

步骤一:创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流:

ObjectInputStream in = new ObjectInputStream(new fileInputStream(“D:\\objectfile.obj”));

步骤二:通过对象输出流的readObject()方法读取对象:

String obj1 = (String)in.readObject();
Date obj2 = (Date)in.readObject();

说明:为了正确读取数据,完成反序列化,必须保证向对象输出流写对象的顺序与从对象输入流中读对象的顺序一致。

序列化实例

假设我们现在有一个需要实例化的实体类Student:

public class Student implements Serializable{
    //设置唯一的序列化标志
    private static final long serialVersionUID = 42L;

    private long studentId;
    private String name;
    private Sex sex;  //这里使用了枚举类型
    private transient String hobby; //注意这里的transient关键字

    public Student() {}
    public Student(long studentId, String name, Sex sex, String hobby) {
        this.studentId = studentId;
        this.name = name;
        this.sex = sex;
        this.hobby = hobby;
    }
    //getter、setter方法
    public long getStudentId() {return studentId;}

    public void setStudentId(long studentId) {this.studentId = studentId;}

    public String getName() {return name;}

    public void setName(String name) {this.name = name;}

    public Sex getSex() {return sex;}

    public void setSex(Sex sex) {this.sex = sex;}

    public String getHobby() {return hobby;}

    public void setHobby(String hobby) {this.hobby = hobby;}

   /* private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        //做一些特别的操作,比如说把性别改成FEMALE
        this.sex = Sex.FEMALE;
        out.writeObject(this);
    }*/

    /*private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        Student stu = (Student) in.readObject();
        stu.name = "dodoCheng";  //做一些特别的操作,比如说将名字改为”dodoCheng"
    }*/
}

性别Sex所使用的枚举类型如下:

public enum Sex {
    MALE,FEMALE;
}

下面是序列化的测试类:

public class SerializeTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Student outStu = new Student(201531060634L,"dodoZhou",Sex.MALE,"篮球");
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\stu.txt"));
        System.out.println("序列化之前:");
        System.out.println("studentId:"+outStu.getStudentId());
        System.out.println("name:"+ outStu.getName());
        System.out.println("sex:"+ outStu.getSex());
        System.out.println("hobby:"+ outStu.getHobby());
        oos.writeObject(outStu);
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\stu.txt"));
        Student inStu = (Student)ois.readObject();
        System.out.println("反序列化之后:");
        System.out.println("studentId:"+inStu.getStudentId());
        System.out.println("name:"+ inStu.getName());
        System.out.println("sex:"+ inStu.getSex());
        System.out.println("hobby:"+ inStu.getHobby());

    }
}

它的运行结果为:

序列化之前:
studentId:201531060634
name:dodoZhou
sex:MALE
hobby:篮球
反序列化之后:
studentId:201531060634
name:dodoZhou
sex:MALE
hobby:null

可以看到:

  • transient关键字修饰的字段没有被序列化。
  • 在序列化中,枚举类型工作良好



参考文章:
https://blog.csdn.net/dont27/article/details/38309061
https://blog.csdn.net/zhoucheng05_13/article/details/79780147
https://blog.csdn.net/wangloveall/article/details/7992448/

你可能感兴趣的:(语言——Java——基础)