一、概述

序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。

为什么要把Java对象序列化呢?因为序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程。

有序列化,就有反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象。有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。

我们来看看如何把一个Java对象序列化。

一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口,它的定义如下:

public interface Serializable {
}

Serializable接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。

在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用此对象。但是,我们创建出来的这些对象都存在于JVM中的堆(heap)内存中,只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止,这些对象也就随之消失;但是在真实的应用场景中,我们需要将这些对象持久化下来,并且在需要的时候将对象重新读取出来,Java的序列化可以帮助我们实现该功能。

在JAVA中,对象的序列化和反序列化被广泛的应用到RMI(远程方法调用)及网络传输中 

二、序列化及反序列化相关的接口和类

Java为了方便开发人员将java对象序列化及反序列化提供了一套方便的API来支持,其中包括以下接口和类:

java.io.Serializable

java.io.Externalizable

ObjectOutput

ObjectInput

ObjectOutputStream

ObjectInputStream

三、Serialization接口详解

Java类通过实现java.io.Serialization接口来启用序列化功能,未实现此接口的类将无法将其任何状态或者信息进行序列化或者反序列化。

当试图对一个对象进行序列化时,如果遇到一个没有实现java.io.Serialization接口的对象时,将抛出NotSerializationException异常。

如果要序列化的类有父类,要想将在父类中定义过的变量序列化下来,那么父类也应该实现java.io.Serialization接口。

package com.example.anonotationnormal.seriable;

import java.io.Serializable;

/**
 * @author :luoyu
 * @version :1.0
 * @date : 2021/10/12 12:18 下午
 * @description
 */

public class Person implements Serializable {
    private String name;
    private int age;

    public Person() {
        System.out.println("无参构造.");
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("有参构造.");
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

序列化:

package com.example.anonotationnormal.seriable;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

/**
 * @author :luoyu
 * @version :1.0
 * @date : 2021/10/12 12:19 下午
 * @description
 */

public class MainTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person = new Person("luoyu", 24);

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("abc.txt"));
        oos.writeObject(person);
        oos.close();
    }
}

运行结果:【同时将对象序列化到指定的文件里面】


反序列化:

package com.example.anonotationnormal.seriable;

import java.io.*;

/**
 * @author :luoyu
 * @version :1.0
 * @date : 2021/10/12 12:19 下午
 * @description
 */

public class MainTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("abc.txt"));
        Person p = (Person) ois.readObject();
        System.out.println(p);
    }
}

运行结果:

Person{name='luoyu', age=24} 

这里体现了构造函数在序列化与反序列化中的作用,或者说,反序列化的对象是由 JVM 自己生成的对象,而不会通过构造方法生成。 还有一点需要注意:反序列化Person对象时,需要能够找到Person.class,否则会抛出ClassNotFoundException 异常。

成员类相关序列化

如果一个可序列化的类的成员不是基本类型,而是引用类型,则这个引用类型也必须实现

Person.java

package com.example.anonotationnormal.seriable;

/**
 * @author :luoyu
 * @version :1.0
 * @date : 2021/10/12 12:18 下午
 * @description
 */

public class Person {
    private String name;
    private int age;

    public Person() {
        System.out.println("无参构造.");
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("有参构造.");
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

Teacher.java 

package com.example.anonotationnormal.seriable;

import java.io.Serializable;

/**
 * @author :luoyu
 * @version :1.0
 * @date : 2021/10/12 1:12 下午
 * @description
 */

public class Teacher implements Serializable {
    private String name;
    private Person person;

    public Teacher(String name, Person person) {
        this.name = name;
        this.person = person;
    }

    public String getName() {
        return name;
    }

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

    public Person getPerson() {
        return person;
    }

    public void setPerson(Person person) {
        this.person = person;
    }

    @Override
    public String toString() {
        return "Teacher{" +
                "name='" + name + '\'' +
                ", person=" + person +
                '}';
    }
}

MainTest.java(序列化测试类)

package com.example.anonotationnormal.seriable;

import java.io.*;

/**
 * @author :luoyu
 * @version :1.0
 * @date : 2021/10/12 12:19 下午
 * @description
 */

public class MainTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person = new Person("luoyu", 24);
        Teacher t = new Teacher("xiaoyu", person);

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("abc.txt"));
        oos.writeObject(t);
        oos.close();
    }
}

运行结果:

有参构造.
Exception in thread "main" java.io.NotSerializableException: com.example.anonotationnormal.seriable.Person
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
    at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
    at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
    at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
    at com.example.anonotationnormal.seriable.MainTest.main(MainTest.java:18) 

对序列化机制来说,如果对同一对象执行多次序列化操作,并不会得到多个对象。因为保存到磁盘的对象都有一个序列化编号,当程序试图进行序列化时,会检查该对象是否序列化过,只有该对象从未被序列化过,才会将此对象系列化为字节序列,如果此对象已经序列化过,则直接输出其序列化编号。

四、自定义序列号策略Externalizable 

先来看看案例:

package com.example.anonotationnormal.seriable;

import java.io.*;

/**
 * @author :luoyu
 * @version :1.0
 * @date : 2021/10/12 12:18 下午
 * @description
 */

public class Person implements Externalizable {
    private String name;
    private int age;

    public Person() {
        System.out.println("无参构造.");
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("有参构造.");
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {

    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {

    }
}

序列化:

package com.example.anonotationnormal.seriable;

import java.io.*;

/**
 * @author :luoyu
 * @version :1.0
 * @date : 2021/10/12 12:19 下午
 * @description
 */

public class MainTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person = new Person("luoyu", 24);
        System.out.println(person);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("abc.txt"));
        oos.writeObject(person);
        oos.close();
    }
}

运行结果:

有参构造.
Person{name='luoyu', age=24} 

反序列化:

package com.example.anonotationnormal.seriable;

import java.io.*;

/**
 * @author :luoyu
 * @version :1.0
 * @date : 2021/10/12 12:19 下午
 * @description
 */

public class MainTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("abc.txt"));
        Person p = (Person) ois.readObject();
        System.out.println(p);
    }
}

 运行结果:

无参构造.
Person{name='null', age=0}

总结:先进性序列化后进行反序列化,对象的属性都恢复成了默认值,也就是说之前对象的状态并没有被持久化下来,这就是Externalization和Serialization接口之间的区别;

Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法。由于上面的代码中,并没有在这两个方法中定义序列化实现细节,所以输出的内容为空
 

还有一点值得注意:在使用Externalizable进行反序列化的时候,在读取对象时,会调用对象的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。所以,实现Externalizable接口的类必须要提供一个public的无参的构造器。

如果我们在序列化的过程中有一些别的需求,或者说,我们希望对象的一部分可以被序列化,而另一部分不被序列化,此时可以实现 Externalizable 接口,并且实现它的两个方法:writeExternal() 和 readExternal(),这两个方法会在序列化和反序列化的过程中被自动调用以便执行一些特殊的操作。

适配Externalizable修改代码:

Person.java

package com.example.anonotationnormal.seriable;

import java.io.*;

/**
 * @author :luoyu
 * @version :1.0
 * @date : 2021/10/12 12:18 下午
 * @description
 */

public class Person implements Externalizable {
    private String name;
    private int age;

    public Person() {
        System.out.println("无参构造.");
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("有参构造.");
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", 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 {
        name = (String)in.readObject();
        age = in.readInt();
    }
}

序列化:

package com.example.anonotationnormal.seriable;

import java.io.*;

/**
 * @author :luoyu
 * @version :1.0
 * @date : 2021/10/12 12:19 下午
 * @description
 */

public class MainTestOut {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person = new Person("luoyu", 24);
        System.out.println(person);

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("abc.txt"));
        oos.writeObject(person);
        oos.close();
    }
}

运行结果:

有参构造.
Person{name='luoyu', age=24}

反序列化:

package com.example.anonotationnormal.seriable;

import java.io.*;

/**
 * @author :luoyu
 * @version :1.0
 * @date : 2021/10/12 12:19 下午
 * @description
 */

public class MainTestIn {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("abc.txt"));
        Person p = (Person) ois.readObject();
        System.out.println(p);
    }
}

运行结果:

无参构造.
Person{name='luoyu', age=24} 

五、transient关键字

当然,我们也可以使用transient关键字,将一些重要的信息(如密码)不被进行序列化,如果某个属性被transient关键字修饰的话,则该属性不会参与到序列化的过程,此时将其进行反序列化后,如果该属性是引用数据类型,则返回的是 null,如果该属性是基本数据类型(如 int 类型),则会返回默认值 0(当然,boolean 的默认值是 false)。

public class Person implements Serializable {
    private transient String name;
    private int age;

    public Person() {
        System.out.println("无参构造.");
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("有参构造.");
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

序列化:

public class MainTestOut {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person = new Person("luoyu", 24);
        System.out.println(person);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("abc.txt"));
        oos.writeObject(person);
        oos.close();
    }
}

运行结果:

有参构造.
Person{name='luoyu', age=24} 

反序列化:

public class MainTestIn {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("abc.txt"));
        Person p = (Person) ois.readObject();
        System.out.println(p);
    }
}

运行结果:

Person{name='null', age=24}

可以看到,被transient修饰,所以在进行反序列化输出的时候,均输出了默认值 null 。

由于实现 Externalizable 接口的对象在默认情况下不保存它们的任何字段,所以transient关键字只能和 Serializable 对象一起使用。

六、静态变量的序列化

对象序列化时并不会序列化静态变量,这是因为对象序列化操作是序列化的是对象的状态,而静态变量属于类变量,也就是类的状态。因此,对象序列化并不会保存静态的变量。

public class Test implements Serializable {
    private static int age = 25;

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("staticSerializable.txt"));
        oos.writeObject(new Test());
        oos.close();

        // 修改静态变量的值
        Test.age = 30;

        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("staticSerializable.txt"));
        Test ss = null;
        ss = (Test) ois.readObject();

        System.out.println(ss.age);
    }
}

运行结果:

30

七、序列化ID

序列化ID起着关键的作用,它决定着是否能够成功反序列化!简单来说,java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。

序列化ID如何产生?

当我们一个实体类中没有显示的定义一个名为“serialVersionUID”、类型为long的变量时,Java序列化机制会根据编译时的class自动生成一个serialVersionUID作为序列化版本比较,但是:当我们编写一个类时,随着时间的推移,我们因为需求改动,需要在本地类中添加其他的字段,这个时候再反序列化时便会出现serialVersionUID不一致,导致反序列化失败。那么如何解决呢?便是在本地类中添加一个“serialVersionUID”变量,值保持不变,便可以进行序列化和反序列化。

序列化ID验证?

Person.java

public class Person implements Serializable {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

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

    public Person() {
        System.out.println("无参构造.");
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("有参构造.");
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

MainTestOut.java 序列化

public class MainTestOut {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person = new Person("luoyu", 24);
        System.out.println(person);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("abc.txt"));
        oos.writeObject(person);
        oos.close();

        System.out.println("序列化成功");
    }
}

运行结果:

有参构造.
Person{name='luoyu', age=24}
序列化成功

远程端进行反序列化。

但是Person.java是这种【调整了信息内容】

public class Person implements Serializable {
    private int age;

    public Person() {
        System.out.println("无参构造.");
    }

    public Person(int age) {
        this.age = age;
        System.out.println("有参构造.");
    }

    @Override
    public String toString() {
        return "Person{" +
                ", age=" + age +
                '}';
    }
}

反序列化解析:

public class MainTestIn {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("abc.txt"));
        Person p = (Person) ois.readObject();
        System.out.println(p);
    }
}

运行结果:

Exception in thread "main" java.io.InvalidClassException: com.example.anonotationnormal.seriable.Person; local class incompatible: stream classdesc serialVersionUID = -5891730968650295745, local class serialVersionUID = -5750146077190055238
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2001)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1848)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2158)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)
    at com.example.anonotationnormal.seriable.MainTestIn.main(MainTestIn.java:15)

可知:上述异常的原因是 serialVersionUID 不一致

public class Person implements Serializable {
    private int age;

    private static final long serialVersionUID = -5809782578272943999L;
         //很重要。可以避免上述异常问题
}

总结

       虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致