java.io.Serializable 接口是一个标志性接口,在接口内部没有定义任何属性与方法。只是用于标识此接口的实现类可以被序列化与反序列化。但是它的奥秘并非像它表现的这样简单。现在从以下几个问题入手来考虑。

  1. 希望对象的某些属性不参与序列化应该怎么处理?
  2. 对象序列化之后,如果类的属性发生了增减那么反序列化时会有什么影响呢?
  3. 如果父类没有实现 java.io.Serializable 接口,子类实现了此接口,那么父类中的属性能被序列化吗?
  4. serialVersionUID 属性是做什么用的?必须申明此属性吗?如果不申明此属性会有什么影响?如果此属性的值发生了变化会有什么影响?
  5. 能干预对象的序列化与反序列化过程吗?

在解决这些问题之前,先来看一看如何进行对象的序列化与反序列化。定义一个 Animal 类,并实现 java.io.Serializable 接口。如下代码所示把 Animal 实例序列化为文件保存在硬盘中。

class Animal implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = 8822818790694831649L;
    private String name;
    private String color;
    private String[] alias;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
    public String[] getAlias() {
        return alias;
    }
    public void setAlias(String[] alias) {
        this.alias = alias;
    }
}

Animal 对象进行序列化与反序列化的代码如下所示:

// 反序列化
static void unserializable() throws FileNotFoundException, IOException, ClassNotFoundException{
    ObjectInputStream ois = null;
    try {
        ois = new ObjectInputStream(new FileInputStream("D://animal.dat"));
        Animal animal = (Animal) ois.readObject();
        System.out.println(animal);
    } finally {
        if( null != ois ){
            ois.close();
        }
    }
    
}
// 序列化
static void serializable() throws FileNotFoundException, IOException{
    ObjectOutputStream oos = null;
    try {
        oos = new ObjectOutputStream(new FileOutputStream("D://animal.dat"));
        Animal animal = new Animal();
        animal.setName("Dog");
        animal.setColor("Black");
        animal.setAlias(new String[]{"xiaoHei", "Gou", "GuaiGuai"});
        oos.writeObject(animal);
        oos.flush();
    } finally {
        if(null != oos){
            oos.close();
        }
    }
}

现在利用以上序列化与反序列化 Animal 对象的例子来逐步回答本文开始时提出的几个问题。

一、如何让某些属性不参与序列化与反序列化的过程?

假定在 Animal 对象中,我们希望 alias 属性不能被序列化。这个问题非常容易解决,只需要使用 transient 关键定修饰此属性就可以了。对 Animal 类的简单修改如下所示:

class Animal implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = 8822818790694831649L;
    private String name;
    private String color;
    private transient String[] alias;   
}

如果一个属性被 transient 关键字修饰,那么此属性就不会参与对象序列化与反序列化的过程。

二、类的属性发生了增减那么反序列化时会有什么影响?

假定在设计 Animal 类的时候由于考虑不周全而需要添加 age 属性,那么如果在添加此之前 Animal 对象已序列化为 animal.dat 文件,那么在添加 age 属性之后,还能不能成功的反序列化呢?新的 Animal 类的片段如下所示:

class Animal implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = 8822818790694831649L;
    private String name;
    private String color;
    private transient String[] alias;
    private int age;
}

再次调用反序列化的方法,使用添加 age 属性之前的 animal.dat 文件进行反序列化,运行结果表明还是能正常的反序列化,只是新添加的属性为默认值。

反过来考虑,如果把 animal.dat 文件中存在的 name 属性删除,那么还能使用 animal.dat 文件进行反序列化吗?修改之后的 Animal 类如下所示:

class Animal implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = 8822818790694831649L;
//    private String name;
    private String color;
    private transient String[] alias;
    private int age;
}

调用反序列化的方法,使用删除 name 属性之前的 animal.dat 文件进行反序列化,运行结果表时还是能正常的反序列化。由此可知,类的属性的增删并不能对对象的反序列化造成影响。

三、继承关系在序列化过程中的影响?

假定有父类 Living 没有实现 java.io.Serializable 接口,子类 Human 实现了 java.io.Serializable 接口,那么在序列化子类时父类中的属性能被序列化吗?先给出 LivingHuman 类的定义如下所示:

class Living{
    private String environment;
    public String getEnvironment() {
        return environment;
    }
    public void setEnvironment(String environment) {
        this.environment = environment;
    }
}
class Human extends Living implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = -4389621464687273122L;
    
    private String name;
    private double weight;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getWeight() {
        return weight;
    }
    public void setWeight(double weight) {
        this.weight = weight;
    }
    @Override
    public String toString() {
        return getEnvironment() + " : " + name + ", " + weight;
    }
}

通过代码序列化 Human 对象得到 human.dat 文件,再从此文件中进行反序列化得出结果为:

null : Wg, 130.0

也可以使用文件编辑工具 Notepad++,查看 human.dat 文件如下所示:

在这个文件中看不到任何与父类中的 environment 属性相同的内容,说明这个属性并没有被序列化。

修改父类 Living ,使之实现 java.io.Serialazable 接口,父类修改之后代码片段如下所示:

class Living implements Serializable{
    private String environment;
    public String getEnvironment() {
        return environment;
    }
    public void setEnvironment(String environment) {
        this.environment = environment;
    }
}

序列化 Human 对象再次得到 human.dat 文件,再从此文件中反序列化得出结果为:

human environment : Wg, 130.0

再次通过 Notepad++,查看 human.dat 文件如下所示:

从这个文件中也可以清楚的看到父类 Living 中的 environment 属性被成功的序列化。

由此可得出结论在继承关系中如果父类没有实现 java.io.Serializable 接口,那么在序列化子类时即使子类实现了 java.io.Serializable 接口也不能把父类中的属性序列化。

四、serialVersionUID 属性

在使用 Eclipse 之类的 IDE 开发工具时,如果类实现了 java.io.Serializable 接口,那么 IDE 会警告让生成如下属性:

private static final long serialVersionUID = 8822818790694831649L;

这个属性必须被申明为 static 的,最好是 final 不可修改的。此属性被用于序列化与反序列化过程中的类信息校验,如果此属性的值在序列化之后发生了变化,那么可序列化的文件就不能再反序列化,会抛出 InvalidClassException 异常。如下所示,在序列化之生修改此属性,运行代码的结果:

// 序列化之生手动修改了 serialVersionUID 属性
    private static final long serialVersionUID = 1822818790694831649L;
//    private static final long serialVersionUID = 8822818790694831649L;

这时反序列化会出现如下的异常信息:

java.io.InvalidClassException: j2se.Animal; local class incompatible: stream classdesc serialVersionUID = 8822818790694831649, local class serialVersionUID = 1822818790694831649
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:621)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1623)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1774)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
    at j2se.SerializableTest.unserializable(SerializableTest.java:58)
    at j2se.SerializableTest.animalUnSerializable(SerializableTest.java:50)
    at j2se.SerializableTest.main(SerializableTest.java:26)

由此可见,如果序列化之后修改了 serialVersionUID 属性,那么序列化的文件就不能再成功的反序列化。

当然,在工作中也见过很多程序员并不在意 IDE 警告,不会为类申明 serialVersionUID 属性,因为这个属性也不是必须的。通过把类的 serialVersionUID``属性删除也可以成功的序列化与反序列化,如果类没有显式的申明serialVersionUID 属性,那么 JVM 会依据类的各方面信息自动生成 serialVersionUID 属性值,但是由于不同的 JVM 生成 serialVersionUID 的原理存在差异。所以强烈建议程序员显式申明 serialVersionUID 属性,并强烈建议使用 private static final 修饰此属性。

五、如果干预对象的序列化与反序列化过程?

在上面例子中的 Animal 类中定义了一个由 transient 关键字修饰的 alias 变量,由于被 transient 修饰所以它不会被序列化。但是希望在序列化的过程中把 alias 数组的各个元素序列化,并在反序列化过程把数组中的元素还原到 alias 数组中。 java.io.Serializable 接口虽然没有定义任何方法,但是可以通过在要序列化的类中的申明如下准确签名的方法:

/**
 * 序列化对象时调用此方法完成序列化过程
 * @param o
 * @throws IOException
 */
private void writeObject(ObjectOutputStream o) throws IOException{
}
/**
 * 反序列化对象时调用此方法完成反序列化过程
 * @param o
 * @throws IOException
 * @throws ClassNotFoundException
 */
private void readObject(ObjectInputStream o) throws IOException, ClassNotFoundException{
}
/**
 * 反序列化的过程中如果没有数据时调用此方法
 * @throws ObjectStreamException
 */
private void readObjectNoData() throws ObjectStreamException{
}

Animal 类中可以申明以上的方法,如下所示:

class Animal implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = 8822818790694831649L;
    private String name;
    private String color;
    private transient String[] alias;
    private int age;
    /**
     * 序列化对象时调用此方法完成序列化过程
     * @param o
     * @throws IOException
     */
    private void writeObject(ObjectOutputStream o) throws IOException{
        o.defaultWriteObject(); // 默认写入对象的信息
        o.writeInt(alias.length);// 写入 alias 元素的个数
        for(int i=0;i<alias.length;i++){
            o.writeObject(alias[i]);// 写入 alias 数组中的每一个元素
        }
    }
    /**
     * 反序列化对象时调用此方法完成反序列化过程
     * @param o
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private void readObject(ObjectInputStream o) throws IOException, ClassNotFoundException{
        // 读取顺序与写入顺序一致
        o.defaultReadObject(); // 默认读取对象的信息
        int length = o.readInt(); // 读取 alias 元素的个数
        alias = new String[length];
        for(int i=0;i<length;i++){
            alias[i] = o.readObject().toString(); // 读取元素存入数组
        }
    }
}

到目前为止,我们已可以自定义对象的序列化与反序列化的过程。比如通过以下程序序列化对象,得到 animal.dat 文件。

static void animalSerializable(){
    Animal animal = new Animal();
    animal.setName("Dog");
    animal.setColor("Black");
    animal.setAge(100);
    animal.setAlias(new String[]{"xiaoHei", "Gou", "GuaiGuai"});
    serializable(animal, "D://animal.dat");
}

通过 Notepad++ 打开 animal.dat 文件如下图所示:

可以从上图中发现,实际上可以序列化的文件中找到部分对象信息。现在我们希望能把信息加密之后再序列化,并在反序列化时自动解密。在 java.io.Serializable 接口的实现类中还可以定义如下的方法,用于替换序列化过程中的对象与解析反序列化过程中的对象。

/**
 * 在 writeObject 方法之前调用,通过此方法替换序列化过程中需要替换的内部。
 * @return
 * @throws ObjectStreamException
 */
Object writeReplace() throws ObjectStreamException{
}
/**
 * 在 readObject 方法之前调用,用于把 writeReplace 方法中替换的对象还原
 * @return
 * @throws ObjectStreamException
 */
Object readResolve() throws ObjectStreamException{
}

Animal 对象的序列化与反序列化的过程中可以利用以上的两个方法进行加密与解密,如下所示:

/**
 * 在 writeObject 方法之前调用,通过此方法替换序列化过程中需要替换的内部。
 * @return
 * @throws ObjectStreamException
 */
Object writeReplace() throws ObjectStreamException{
    try {
        Animal animal = new Animal();
        String key = String.valueOf(serialVersionUID); // 简单使用 erialVersionUID 做为对称算法的密钥 
        animal.setAge(getAge() << 2); // 对于整数就简单的处理为向左移动两位
        animal.setName(DesUtil.encrypt(getName(), key)); // 加密
        animal.setColor(DesUtil.encrypt(getColor(), key));
        String[] as = new String[getAlias().length];
        for(int i=0;i<as.length;i++){
            as[i] = DesUtil.encrypt(getAlias()[i], key);
        }
        animal.setAlias(as);
        return animal;
    } catch (Exception e) {
        throw new InvalidObjectException(e.getMessage());
    }
}
/**
 * 在 readObject 方法之前调用,用于把 writeReplace 方法中替换的对象还原
 * @return
 * @throws ObjectStreamException
 */
Object readResolve() throws ObjectStreamException{
    try {
        Animal animal = new Animal();
        String key = String.valueOf(serialVersionUID);
        animal.setAge(getAge() >> 2);
        animal.setName(DesUtil.decrypt(getName(), key)); // 解密
        animal.setColor(DesUtil.decrypt(getColor(), key));
        String[] as = new String[getAlias().length];
        for(int i=0;i<as.length;i++){
            as[i] = DesUtil.decrypt(getAlias()[i], key);
        }
        animal.setAlias(as);
        return animal;
    } catch (Exception e) {
        throw new InvalidObjectException(e.getMessage());
    }
}

再次使用 Notepad++ 打开 animal.dat 文件如下图所示,在其中就不会再存在 Animal 对象的信息。

所以综上所述,对象的序列化与反序列化过程是完全可控的,利用 writeReplacewriteObject 方法控制序列化过程, readResolvereadObject 方法控制反序列化过程。在序列化过程中与反序列化过程中方法的调用顺序如下所示:

序列化过程: writeReplace –> writeObject

反序列化过程: readObject –> readResolve

本博客复制 JDK1.8 java.io.Serializable 接口详解