天天看点

Java高级系列——不得不说的对象序列化(serialize)

1、什么是Java对象序列化?

Java的对象序列化是将那些实现了Serializable接口的对象转化成一个字节序列,并能够在以后将这些字节序列完全恢复成原来的对象。简单来说序列化就是将对象转化成字节流,反序列化就是将字节流转化成对象。

对象必须在程序中显示的序列化(serialize)和反序列化(deserialize)。

2、序列化的作用

序列化的主要用途主要有两个,一个是对象持久化,另一个是跨网络的数据交换、远程过程调用。

对象持久化意味着一个对象的生存周期并不取决于程序是否正在执行,他可以生存与程序的调用之间。通过将一个序列化的对象写入磁盘,然后在重新调用程序时恢复该对象,就能够实现持久化的效果。

序列化能够弥补不同操作系统之间的差异,比如说可以在运行Windows系统的计算机上创建一个对象,然后将其序列化,通过网络将它发送给一台运行Linux系统的计算机,然后在那里准确的重新组装而不必担心数据在不同的机器上的表示会不同,也不必关心字节的顺序或者其他任何细节,使得对象在其他机器上就像在本地机器上一样。当向远程对象发送消息时,需要通过对象序列化来传输参数和返回值。

3、基本实现

要让一个类支持序列化,只需要让这个类实现接口java.io.Serializable,Serializable没有定义任何方法,只是一个标记接口。

对象序列化是基于字节的,因此要使用OutputStream和InputStream继承层次结构。

序列化对象:创建某些OutputStream对象,然后将其封装在一个ObjectOutputStream对象内,之后调用writeObject()方法序列化对象。

反序列化对象:创建某些InputStream对象,然后将其封装在一个ObjectInputStream对象内,之后调用readObject()方法反序列化对象。反序列化最后获得的是一个指向Object的引用,所以最后必须向下转型为指定类型的对象。

我们定义一个Student类,让该类实现Serializable接口,然后我们通过以上所说的序列化和反序列化方法来亲身感受一下序列化的魔力。

package io;
import java.io.Serializable;
public class Student implements Serializable {
    /**
     * @Comment 
     * @Author Ron
     * @Date 2018年4月9日 上午11:41:41
     */
    private static final long serialVersionUID = L;

    public Student(String no,String name,String className) {
        this.no = no;
        this.name = name;
        this.className = className;
    }

    public String toString() {
        return "HashCode:"+hashCode()+" 学号:"+no+" 姓名:"+name+" 班级:"+className;
    }

    /**
     * 学号
     */
    private String no;

    /**
     * 名字
     */
    private String name;

    /**
     * 班级
     */
    private String className;

    public String getNo() {
        return no;
    }

    public void setNo(String no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

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

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }
}
           

我们定义一个序列化的测试类SerializableTest,具体实现如下:

package io;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class SerializableTest {

    /**
     * @Comment 序列化
     * @Author Ron
     * @Date 2018年4月9日 上午11:45:38
     * @return
     */
    public static void writeStudents(List<Student> students) throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("students.dat"));

        out.writeObject(students);

        out.close();
    }

    /**
     * @Comment 反序列化对象
     * @Author Ron
     * @Date 2018年4月9日 上午11:49:07
     * @return
     */
    public static List<Student> readStudents() throws IOException,ClassNotFoundException  {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("students.dat"));

        List<Student> list = (List<Student>) in.readObject();

        in.close();

        return list;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        List<Student> students = new ArrayList<>(Arrays.asList(new Student("001", "Ron", "Class 001"),new Student("002", "Ron2", "Class 002")));
        System.out.println("------------------序列化前--------------");
        System.out.println(students);
        System.out.println("------------------反序列化后--------------");
        writeStudents(students);
        List<Student> students1 = readStudents();
        System.out.println(students1);
    }
}
           

运行上面的程序我们会得到一个类似于如下的结果:

------------------序列化前--------------
[HashCode:366712642 学号:001 姓名:Ron 班级:Class 001, HashCode:1829164700 学号:002 姓名:Ron2 班级:Class 002]
------------------反序列化后--------------
[HashCode:1836019240 学号:001 姓名:Ron 班级:Class 001, HashCode:325040804 学号:002 姓名:Ron2 班级:Class 002]
           

我们通过结果可以看到,序列化前和序列化后再通过反序列化重新获得的对象数据基本是一致的(这里需要注意,序列化前的对象和反序列化重新获取的对象,在他们的字段数据上是一致的,但是需要注意的是,反序列化是重新生成了对象,并不修改原来的对象)。

序列化时我们需要知道的是:ObjectOutputStream是OutputStream的子类,但实现了ObjectOutput接口,ObjectOutput是DataOutput的子接口,增加了一个方法:

public void writeObject(Object obj) throws IOException
           

这个方法能够将对象obj转化为字节,写到流中。

反序列化时我们需要知道的是:ObjectInputStream是InputStream的子类,它实现了ObjectInput接口,ObjectInput是DataInput的子接口,增加了一个方法:

public Object readObject() throws ClassNotFoundException, IOException
           

这个方法能够从流中读取字节,转化为一个对象。

4、复杂对象

我们上面说描述的是一个非常简单的实例,我们现在来考虑一个稍微复杂一点的情况。如果现在有两个Student对象,这两个Student对象都引用了同一个DeskTop对象(两个同学一张课桌),那么反序列化之后还能让反序列化之后的两个Student对象引用了同一个DeskTop对象吗?我们来看一下。

public class DeskTop implements Serializable {
    /**
     * @Comment 
     * @Author Ron
     * @Date 2018年4月9日 下午1:58:42
     */
    private static final long serialVersionUID = L;
    private Double height;
    private Double width;
    private Double length;

    public String toString() {
        return "height:"+height+" width:"+width+" length:"+length;
    }

    public Double getHeight() {
        return height;
    }
    public void setHeight(Double height) {
        this.height = height;
    }
    public Double getWidth() {
        return width;
    }
    public void setWidth(Double width) {
        this.width = width;
    }
    public Double getLength() {
        return length;
    }
    public void setLength(Double length) {
        this.length = length;
    }
}
           

要让Student能够引用DeskTop对象,我们需要改造一下上面我们的Student类(增加DeskTop属性,修改构造函数)如下;

public Student(String no,String name,String className,DeskTop deskTop) {
    this.no = no;
    this.name = name;
    this.className = className;
    this.setDeskTop(deskTop);
}
private DeskTop deskTop;
           

我们创建两个Student对象和一个DeskTop对象,让两个Student引用同一个DeskTop对象。

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

        DeskTop deskTop = new DeskTop();
        deskTop.setHeight(d);
        deskTop.setLength(d);
        deskTop.setWidth(d);

        Student stdu1 = new Student("001", "Ron", "Class 001",deskTop);
        Student stdu2 = new Student("002", "Ron02", "Class 002",deskTop);

        System.out.println("------------------序列化前--------------");
        if(stdu1.getDeskTop().equals(stdu2.getDeskTop())){
            System.out.println("reference the same object:"+stdu2.getDeskTop());
        }

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("students.dat"));

        out.writeObject(stdu1);
        out.writeObject(stdu2);

        out.close();
        System.out.println("------------------反序列化后--------------");

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("students.dat"));
        Student stud3 = (Student) in.readObject();
        Student stud4 = (Student) in.readObject();
        in.close();

        if(stud3.getDeskTop().equals(stud4.getDeskTop())){
            System.out.println("reference the same object:"+stud4.getDeskTop());
        }
    }
}
           

运行程序,我们会得到如下的结果:

------------------序列化前--------------
reference the same object:height:100.0 width:50.0 length:200.0
------------------反序列化后--------------
reference the same object:height:100.0 width:50.0 length:200.0
           

这也是Java序列化机制的神奇之处,它能自动处理这种引用同一个对象的情况。更神奇的是,它

还能自动处理循环引用的情况,这里我们就不给出具体的例子,读者可自行事件。

5、序列化控制

默认的序列化机制已经很强大了,它可以自动将对象中的所有字段自动保存和恢复,但这种默认行为有时候不是我们想要的。比如,对于有些字段,它的值可能与内存位置有关,比如默认的hashCode()方法的返回值,当恢复对象后,内存位置肯定变了,基于原内存位置的值也就没有了意义。还有一些字段,可能与当前时间有关,比如表示对象创建时的时间,保存和恢复这个字段就是不正确的。

还有一些情况,如果类中的字段表示的是类的实现细节,而非逻辑信息,那默认序列化也是不适合的。为什么不适合呢?因为序列化格式表示一种契约,应该描述类的逻辑结构,而非与实现细节相绑定,绑定实现细节将使得难以修改,破坏封装。

Java提供了多种定制序列化的机制,主要的有三种,一种是transient关键字,另外一种是实现Externalizable接口代替实现Serializable,还有一种是实现writeObject和readObject方法。

将字段声明为transient,默认序列化机制将忽略该字段,不会进行保存和恢复。

比如上面的第一个实例中,假设我们在进行序列化和反序列化时不需要保存和恢复no字段的信息,那么我们可以在no字段前面加上一个transient修饰符。

private transient String no;
           

运行程序我们会得到如下的结果:

------------------序列化前--------------
[HashCode: 学号: 姓名:Ron 班级:Class , HashCode: 学号: 姓名:Ron2 班级:Class ]
------------------反序列化后--------------
[HashCode: 学号:null 姓名:Ron 班级:Class , HashCode: 学号:null 姓名:Ron2 班级:Class ]
           

我们可以到no字段的内容反序列化之后变成了null。将字段声明为transient,不是说就不保存该字段了,而是告诉Java默认序列化机制,不要自动保存该字段了。

6、实现Externalizable接口对序列化进行控制

我们定义一个类Student2,其字段信息与我们上述的Student一致,但是不同的是,我们的Student2类实现的是Externalizable接口而不是Serializable。实现Externalizable接口必须实现如下两个方法:

public void writeExternal(ObjectOutput out) throws IOException
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
           

同时实现Externalizable接口必须保证实现的类具有无参构造器,默认情况下如果在你的类中没有显示声明构造器,那么你可以不用关心这个问题。但是如果你已经显示的声明过构造器(比如我们的Student2中声明了有参的构造器,那么我们就必须声明一个无参构造器),那么你就必须声明一个无参的构造器,否则在进行序列化时会报no valid constructor异常。

Student2中关键代码如下:

public class Student2 implements Externalizable {

    /**
     * @Comment 
     * @Author Ron
     * @Date 2018年4月9日 上午11:41:41
     */
    private static final long serialVersionUID = L;

    public Student2() {
        System.out.println("Student2 Constructor");
    }

    public Student2(String no,String name,String className,DeskTop deskTop) {
        this.no = no;
        this.name = name;
        this.className = className;
        this.setDeskTop(deskTop);
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("------------------writeExternal--------------");
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        System.out.println("------------------readExternal--------------");
    }
}
           

我们开发一个测试程序如下:

public class SerializableTest3 {

    /**
     * @Comment 序列化
     * @Author Ron
     * @Date 2018年4月9日 上午11:45:38
     * @return
     */
    public static void writeStudents(List<Student2> students) throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("students.dat"));
        System.out.println("------------------开始序列化--------------");
        out.writeObject(students);
        System.out.println("------------------序列化结束--------------");
        out.close();
    }

    /**
     * @Comment 反序列化对象
     * @Author Ron
     * @Date 2018年4月9日 上午11:49:07
     * @return
     */
    public static List<Student2> readStudents() throws IOException,ClassNotFoundException  {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("students.dat"));
        System.out.println("------------------开始反序列化--------------");
        List<Student2> list = (List<Student2>) in.readObject();
        System.out.println("------------------反序列化结束--------------");
        in.close();

        return list;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        List<Student2> students = new ArrayList<>(Arrays.asList(new Student2("001", "Ron", "Class 001",null),new Student2("002", "Ron2", "Class 002",null)));
        System.out.println("------------------序列化前--------------");
        System.out.println(students);
        writeStudents(students);
        List<Student2> students1 = readStudents();
        System.out.println(students1);
    }
}
           

我们运行程序,会得到如下的结果:

------------------序列化前--------------
[HashCode:366712642 学号:001 姓名:Ron 班级:Class 001, HashCode:1829164700 学号:002 姓名:Ron2 班级:Class 002]
------------------开始序列化--------------
------------------writeExternal--------------
------------------writeExternal--------------
------------------序列化结束--------------
------------------开始反序列化--------------
Student2 Constructor
------------------readExternal--------------
Student2 Constructor
------------------readExternal--------------
------------------反序列化结束--------------
[HashCode:1252169911 学号:null 姓名:null 班级:null, HashCode:2101973421 学号:null 姓名:null 班级:null]
           

通过结果我们可以看到:

  1. 在序列化和反序列化过程中writeExternal(ObjectOutput out)和readExternal(ObjectInput in)会自动的被调用。上面的示例中我们在这两个方法中并未做过多操作,所以通过反序列化获得的结果都为null。
  2. 同时我们还可以发现,在反序列化时,无参的构造器都会先被调用。这与恢复一个Serializable对象不同,对于Serializable对象,对象完全以他存储的二进制为基础来构造,而不调用构造器。而对于一个Externalizable对象,所有普通的无参(默认)构造器都会被调用,然后调用readExternal(ObjectInput in)。必须注意这一点,所有普通的无参(默认)构造器都会被调用,才能使Externalizable对象产生正确的行为。

那如果在反序列化时我们想要完全恢复对象时怎么处理?我们只需要按照正确的方式实现writeExternal(ObjectOutput out)和readExternal(ObjectInput in)即可。我们将Student2 中的这两个方法按照如下修改:

@Override
public void writeExternal(ObjectOutput out) throws IOException {
    System.out.println("------------------writeExternal--------------");
    out.writeObject(no);
    out.writeObject(name);
    out.writeObject(className);
    out.writeObject(deskTop);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    System.out.println("------------------readExternal--------------");
    no = (String) in.readObject();
    name = (String) in.readObject();
    className = (String) in.readObject();
    deskTop = (DeskTop) in.readObject();
}
           

然后我们运行一下上面的程序,我们会得到如下结果:

------------------序列化前--------------
[HashCode:366712642 学号:001 姓名:Ron 班级:Class 001, HashCode:1829164700 学号:002 姓名:Ron2 班级:Class 002]
------------------开始序列化--------------
------------------writeExternal--------------
------------------writeExternal--------------
------------------序列化结束--------------
------------------开始反序列化--------------
Student2 Constructor
------------------readExternal--------------
Student2 Constructor
------------------readExternal--------------
------------------反序列化结束--------------
[HashCode:21685669 学号:001 姓名:Ron 班级:Class 001, HashCode:2133927002 学号:002 姓名:Ron2 班级:Class 002]
           

7、实现Serializable接口的类中添加writeObject和readObject方法

如果不是特别坚持实现Externalizable 接口,那么我们可以在实现了Serializable接口的类中添加如下两个方法来对序列化进行控制。

private void writeObject(ObjectOutputStream stream) throws IOException
private void readObject(ObjectInputStream stream) throws IOException,ClassNotFoundException
           

这样一旦对象被序列化或者被反序列化还原,就会自动的分别调用这两个方法,也就是说只要我们提供这两个方法,就会使用他们而不是默认的序列化机制。

我们先来看例子,我们创建一个类,命名为Student3并实现Serializable接口,类中的属性信息与上文所述的Student一致,不一样的就是在Student3中我们添加了以下两个方法:

private void writeObject(ObjectOutputStream stream) throws IOException {
    System.out.println("------------------Student.writeObject--------------");
    stream.writeObject(no);
}

private void readObject(ObjectInputStream stream) throws IOException,ClassNotFoundException {
    System.out.println("------------------Student.readObject--------------");
    no = (String) stream.readObject();
}
           

我们再新建一个测试类,具体代码如下:

public class SerializableTest4 {

    /**
     * @Comment 序列化
     * @Author Ron
     * @Date 2018年4月9日 上午11:45:38
     * @return
     */
    public static void writeStudent3s(List<Student3> Student3s) throws IOException {
        System.out.println("------------------开始序列化--------------");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Student3s.dat"));

        out.writeObject(Student3s);

        out.close();
        System.out.println("------------------序列化结束--------------");
    }

    /**
     * @Comment 反序列化对象
     * @Author Ron
     * @Date 2018年4月9日 上午11:49:07
     * @return
     */
    public static List<Student3> readStudent3s() throws IOException,ClassNotFoundException  {
        System.out.println("------------------开始反序列化--------------");

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("Student3s.dat"));

        List<Student3> list = (List<Student3>) in.readObject();

        in.close();
        System.out.println("------------------反序列化结束--------------");
        return list;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        List<Student3> Student3s = new ArrayList<>(Arrays.asList(new Student3("001", "Ron", "Class 001",null),new Student3("002", "Ron2", "Class 002",null)));
        System.out.println("------------------序列化前--------------");
        System.out.println(Student3s);

        writeStudent3s(Student3s);
        List<Student3> Student3 = readStudent3s();

        System.out.println(Student3);
    }
}
           

运行该类我们可以得到如下结果:

------------------序列化前--------------
[HashCode:366712642 学号:001 姓名:Ron 班级:Class 001, HashCode:1829164700 学号:002 姓名:Ron2 班级:Class 002]
------------------开始序列化--------------
------------------Student.writeObject--------------
------------------Student.writeObject--------------
------------------序列化结束--------------
------------------开始反序列化--------------
------------------Student.readObject--------------
------------------Student.readObject--------------
------------------反序列化结束--------------
[HashCode:1735600054 学号:001 姓名:null 班级:null, HashCode:21685669 学号:002 姓名:null 班级:null]
           

根据结果我们来分析一下;

  • 首先、我们看到在类Student3中,writeObject(ObjectOutputStream stream)和readObject(ObjectInputStream stream)被定义成private,这意味着他们仅能被这个类的其他成员调用。然而,实际上我们并没有从这个类的其他成员调用他们,而通过运行结果我们可以看到,你的对象的writeObject()和readObject()方法是通过ObjectOutputStream和ObjectInputStream对象的writeObject()和readObject()方法调用的。那么ObjectOutputStream和ObjectInputStream对象是怎样访问你的类的private方法的呢?答案是反射。
  • 在调用ObjectOutputStream.writeObject()时,会检查所传递的Serializable对象,看看是否实现了它自己的writeObject(),如果这样,就跳过正常的序列化过程并调用它的writeObject()。readObject()的情形与此相同。

如果在我们自己的writeObject()内部,可以调用defaultWriteObject()来执行默认的writeObject(),同理,在我们自己的readObject()内部,也可以调用defaultReadObject()来执行默认的readObject()。

如:

private void writeObject(ObjectOutputStream stream) throws IOException {
    System.out.println("------------------Student.writeObject--------------");
    stream.defaultWriteObject();
}

private void readObject(ObjectInputStream stream) throws IOException,ClassNotFoundException {
    System.out.println("------------------Student.readObject--------------");
    stream.defaultReadObject();
}