easy-algorithm-interview-an.../code-languages/java/理解Java对象序列化.md

8.1 KiB
Raw Blame History

关于Java序列化的文章早已是汗牛充栋了本文是对我个人过往学习理解及应用Java序列化的一个总结。此文内容涉及Java序列化的基本原理以及多种方法对序列化形式进行定制。在撰写本文时既参考了Thinking in Java, Effective JavaJavaWorlddeveloperWorks中的相关文章和其它网络资料也加入了自己的实践经验与理解文、码并茂希望对大家有所帮助。(2012.02.14最后更新)

1. 什么是Java对象序列化

Java平台允许我们在内存中创建可复用的Java对象但一般情况下只有当JVM处于运行时这些对象才可能存在这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中就可能要求在JVM停止运行之后能够保存(持久化)指定的对象并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。
使用Java对象序列化在保存对象时会把其状态保存为一组字节在未来再将这些字节组装成对象。必须注意地是对象序列化保存的是对象的“状态”即它的成员变量。由此可知对象序列化不会关注类中的静态变量。
除了在持久化对象时会用到对象序列化之外当使用RMI(远程方法调用)或在网络中传递对象时都会用到对象序列化。Java序列化API为处理对象序列化提供了一个标准机制该API简单易用在本文的后续章节中将会陆续讲到。

2. 简单示例

在Java中只要一个类实现了java.io.Serializable接口那么它就可以被序列化。此处将创建一个可序列化的类Person本文中的所有示例将围绕着该类或其修改版。
Gender类是一个枚举类型表示性别

public enum Gender {
    MALE,FEMALE
}

Person类实现了Serializable接口它包含三个字段nameString类型ageInteger类型genderGender类型。另外还重写该类的toString()方法以方便打印Person实例中的内容。

public class Person implements Serializable{

    private String name = null;

    transient private Integer age = null;

    private Gender gender = null;

    public Person() {
        System.out.println("none-arg constructor");
    }

    public Person(String name,Integer age,Gender gender) {
        System.out.println("arg constructor");
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Gender getGender() {
        return gender;
    }

    public void setGender(Gender gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "[" + name + "," + age + "," + gender + "]";
    }
}

SimpleSerial是一个简单的序列化程序它先将一个Person对象保存到文件person.out中然后再从该文件中读出被存储的Person对象并打印该对象。

public class SimpleSerial {

    public static void main(String[] args) throws Exception{
        File file = new File("person.out");

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
        Person person = new Person("John",18,Gender.MALE);
        out.writeObject(person);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
        Object newPerson = in.readObject();
        in.close();
        System.out.println(newPerson);
    }
}

上述程序的输出的结果为:

arg constructor
[John, 31, MALE]

此时必须注意的是当重新读取被保存的Person对象时并没有调用Person的任何构造器看起来就像是直接使用字节将Person对象还原出来的。
当Person对象被保存到person.out文件中之后我们可以在其它地方去读取该文件以还原对象但必须确保该读取程序的CLASSPATH中包含有Person.class(哪怕在读取Person对象时并没有显示地使用Person类如上例所示)否则会抛出ClassNotFoundException。

3. Serializable的作用

为什么一个类实现了Serializable接口它就可以被序列化呢在上节的示例中使用ObjectOutputStream来持久化对象在该类中有如下代码

private void writeObject0(Object obj, boolean unshared) throws IOException {
    
    if (obj instanceof String) {
        writeString((String) obj, unshared);
    } else if (cl.isArray()) {
        writeArray(obj, desc, unshared);
    } else if (obj instanceof Enum) {
        writeEnum((Enum) obj, desc, unshared);
    } else if (obj instanceof Serializable) {
        writeOrdinaryObject(obj, desc, unshared);
    } else {
        if (extendedDebugInfo) {
            throw new NotSerializableException(cl.getName() + "\n"
                    + debugInfoStack.toString());
        } else {
            throw new NotSerializableException(cl.getName());
        }
    }
    ...
}

从上述代码可知如果被写对象的类型是String或数组或Enum或Serializable那么就可以对该对象进行序列化否则将抛出NotSerializableException。

4. 默认序列化机制

如果仅仅只是让某个类实现Serializable接口而没有其它任何处理的话则就是使用默认序列化机制。使用默认机制在序列化对象时不仅会序列化当前对象本身还会对该对象引用的其它对象也进行序列化同样地这些其它对象引用的另外对象也将被序列化以此类推。所以如果一个对象包含的成员变量是容器类对象而这些容器所含有的元素也是容器类对象那么这个序列化的过程就会较复杂开销也较大。

5. 影响序列化

在现实应用中,有些时候不能使用默认序列化机制。比如,希望在序列化过程中忽略掉敏感数据,或者简化序列化过程。下面将介绍若干影响序列化的方法。

5.1 transient关键字

当某个字段被声明为transient后默认序列化机制就会忽略该字段。此处将Person类中的age字段声明为transient如下所示

public class Person implements Serializable {
    ...
    transient private Integer age = null;
    ...
}

再执行SimpleSerial应用程序会有如下输出

arg constructor
[John, null, MALE]

可见age字段未被序列化。

5.2 writeObject()方法与readObject()方法

对于上述已被声明为transitive的字段age除了将transitive关键字去掉之外是否还有其它方法能使它再次可被序列化方法之一就是在Person类中添加两个方法writeObject()与readObject(),如下所示:

public class Person implements Serializable {
    ...
    transient private Integer age = null;
    ...

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeInt(age);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        age = in.readInt();
    }
}

在writeObject()方法中会先调用ObjectOutputStream中的defaultWriteObject()方法该方法会执行默认的序列化机制如5.1节所述此时会忽略掉age字段。然后再调用writeInt()方法显示地将age字段写入到ObjectOutputStream中。readObject()的作用则是针对对象的读取其原理与writeObject()方法相同。

再次执行SimpleSerial应用程序则又会有如下输出

arg constructor
[John, 31, MALE]

必须注意地是writeObject()与readObject()都是private方法那么它们是如何被调用的呢毫无疑问是使用反射。详情可见ObjectOutputStream中的writeSerialData方法以及ObjectInputStream中的readSerialData方法。

原文链接地址:http://www.blogjava.net/jiangshachina/archive/2012/02/13/369898.html