# 序列化与反序列化

Java 对象是有生命周期的,当生命周期结束它就会被回收,但是可以通过将其转换为字节序列永久保存下来或者通过网络传输给另一方。

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

# Serializable 接口

一个类实现 java.io.Serializable 接口就可以被序列化或者反序列化。实际上,Serializable 接口中没有任何变量和方法,它只是一个标识。如果没有实现这个接口,在序列化或者反序列化时会抛出 NotSerializableException 异常。

下面是一个实现了 Serializable 接口的类以及它的序列化与反序列化过程。

public class SerialTest {
    public static void main(String[] args) {
        Test test = new Test();
        test.setName("test");
        // 序列化,存储对象到文本
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("test"));
            oos.writeObject(test);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (oos != null) {
                    oos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        // 反序列化,从文本中取出对象
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream("test"));
            Test1 test1 = (Test1) ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                if (ois != null) {
                    ois.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
class Test implements Serializable {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Test{" +
                "name='" + name + '\'' +
                '}';
    }
}

运行结果:

Test{name='test'}

# serialVersionUID

private static final long serialVersionUID = -3297006450515623366L;

serialVersionUID 是一个序列化版本号,实现 Serializable 接口的类都会有一个版本号。如果没有自己定义,那么程序会默认生成一个版本号,这个版本号是 Java 运行时环境根据类的内部细节自动生成的。最好我们自己定义该版本号,否则当类发生改变时,程序为我们自动生成的序列化版本号也会发生改变,那么再将原来的字节序列反序列化时就会发生错误。

下面是将 Test1 类加入一个变量 age,此时再进行反序列化的结果。可以看出,序列化版本号已发生改变,程序认为不是同一个类,不能进行反序列化。

java.io.InvalidClassException: test.Test1; local class incompatible: stream classdesc serialVersionUID = 9097989105451761251, local class serialVersionUID = -7756223913249050270
	at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:689)
	at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1903)
	at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1772)
	at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2060)
	at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1594)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:430)
	at test.SerialTest.main(SerialTest.java:11)

为了提高 serialVersionUID 的独立性和确定性,强烈建议在一个可序列化类中显示地定义 serialVersionUID,为他赋予明确的值。

那么在 IDEA 中,怎么手动生成呢?

在 settings->Editor->Inspections 下,搜索 serial,开启 Serializable class without'serialVersionUID' 的拼写检查,然后将光标放在实现 Serializable 的接口上,按住 ALt+Enter 键,选择添加 serialVersionUID 即可。

# Transient 关键字

transient 修饰类的变量,可以使变量不被序列化。反序列化时,被 transient 修饰的变量的值被设为初始值,如 int 类型被设为 0,对象型被设为 null。

# ObjectOutputStream 类和 ObjectInputStream 类

ObjectOutputStream 的 writeObject 方法可以序列化对象,ObjectInputStream 的 readObject 可以反序列化对象。ObjectOutputStream 实现了接口 ObjectOutput,所以可以进行对象写操作。ObjectInputStream 实现了接口 ObjectInput,所以可以对对象进行读操作。

# 静态变量序列化

给 Test 类中增加一个静态变量,赋值为 12,然后在序列化之后修改其值为 10,反序列化之后打印它的值。发现打印的值为 10,之前的 12 并没有被保存。

静态变量是不参与序列化的,序列化只是用来保存对象的状态,而静态变量属于类的状态。

# 父类序列化

让 Test 继承一个没有实现 Serializable 接口的类,设置父类中变量的值,对 Test 类的实例进行序列化与反序列化操作。

public class SerialTest {
    public static void main(String[] args) {
        Test test = new Test();
        test.setName("huihui");
        test.setSex(12);
        // 序列化,存储对象到文本
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("test"));
            oos.writeObject(test);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (oos != null) {
                    oos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        // 反序列化,从文本中取出对象
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream("test"));
            Test test1 = (Test) ois.readObject();
            System.out.println(test1);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                if (ois != null) {
                    ois.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
class Test extends TestFather implements Serializable {
    private static final long serialVersionUID = 4335715933640891747L;
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Test{" +
                "name='" + name + '\'' +
                "sex='" + sex + '\'' +
                '}';
    }
}
class TestFather {
    protected Integer sex;
    public Integer getSex() {
        return sex;
    }
    public void setSex(Integer sex) {
        this.sex = sex;
    }
    @Override
    public String toString() {
        return "TestFather{" +
                "sex='" + sex + '\'' +
                '}';
    }
}

运行结果:

Test{name='huihui'sex='null'}

发现虽然对 sex 进行了复制,但是反序列化结果仍然为 null。

现在让 TestFather 类实现 Serializable 接口,运行结果如下。所以当我们想要序列化父类的变量时,也需要让父类实现 Serializable 接口。

Test{name='huihui'sex='12'}

同理,如果 Test 类中有任何变量是对象,那么该对象的类也需要实现 Serializable 接口。查看 String 源代码,确实实现了 Serializable 接口。大家可以测试一下字段的类不实现 Serializable 接口的情况,运行会直接报 java.io.NotSerializableException 异常。

# 敏感字段加密

如果对于某些字段我们并不想直接暴露出去,需要对其进行加密处理,那么就需要我们自定义序列化和反序列化方法。使用 Serializable 接口进行序列化时,如果不自定义方法,则默认调用 ObjectOutputStream 的 defaultWriteObject 方法和 ObjectInputStream 的 defaultReadObject 方法。下面我们来尝试一下自己实现序列化与反序列化过程。

class Test implements Serializable {
    private static final long serialVersionUID = 4335715933640891747L;
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Test{" +
                "name='" + name + '\'' +
                '}';
    }
    private void writeObject(ObjectOutputStream out) {
        try {
            ObjectOutputStream.PutField putField = out.putFields();
            System.out.println("原name:" + name);
            // 模拟加密
            name = "change";
            putField.put("name", name);
            System.out.println("加密后的name:" + name);
            out.writeFields();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void readObject(ObjectInputStream in) {
        try {
            ObjectInputStream.GetField getField = in.readFields();
            Object object = getField.get("name", "");
            System.out.println("要解密的name:" + object.toString());
            name = "huihui";
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

原name:huihui
加密后的name:change
要解密的name:change
解密后的name:huihui

这种写法重写了 writeObject 方法和 readObject 方法,下面一种接口也可以实现相同的功能。

# Externalizable 接口

除了 Serializable 接口,Java 还提供了一个 Externalizable 接口,它继承了 Serializable 接口,提供了 writeExternal 和 readExternal 两个方法,实现该接口的类必须重写这两个方法。同时还发现,类还必须提供一个无参构造方法,否则会报 java.io.InvalidClassException 异常。

先不深究为什么要加一个无参构造方法,我们先试一下这个接口的序列化效果。将类 Test 改为如下所示:

class Test implements Externalizable {
    private static final long serialVersionUID = 4335715933640891747L;
    private String name;
    public Test() {
    }
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Test{" +
                "name='" + name + '\'' +
                '}';
    }
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
    }
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    }
}

再次运行测试方法,发现输出的 name 是 null。在 readObject 处打断点,发现会调用无参构造方法。

name 其实并没有被序列化与反序列化,writeExternal 方法和 readExternal 方法中是需要我们自己来实现序列化与反序列化的细节的。在反序列化时,会首先调用类的无参考构造方法创建一个新对象,然后再填充每个字段。

我们对 writeExternal 方法和 readExternal 方法进行重写:

@Override
public void writeExternal(ObjectOutput out) throws IOException {
    out.writeObject(name);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    name = (String) in.readObject();
}

此时运行测试方法,发现 Test 类被正常序列化与反序列化。

# 序列化存储规则

当多次序列化一个对象时,是会序列化多次还是会序列化一次呢?

public class SerialTest {
    public static void main(String[] args) {
        Test test = new Test();
        test.setName("huihui");
        // 序列化,存储对象到文本
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("test"));
            // 两次写入文件
            oos.writeObject(test);
            oos.flush();
            System.out.println(new File("test").length());
            oos.writeObject(test);
            oos.flush();
            System.out.println(new File("test").length());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (oos != null) {
                    oos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        // 反序列化,从文本中取出对象
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream("test"));
            // 读取两个对象
            Test test1 = (Test) ois.readObject();
            Test test2 = (Test) ois.readObject();
            System.out.println(test1 == test1);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                if (ois != null) {
                    ois.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
class Test implements Serializable {
    private static final long serialVersionUID = 4335715933640891747L;
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Test{" +
                "name='" + name + '\'' +
                '}';
    }
}

运行结果:

73
78
true

可以发现,当第二次写入对象时,文件的长度仅仅增加了 5 个字节,并且在判等时,两个引用指向同一地址。

Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用。