java序列化

1. 什么是Java对象序列化

Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。利用对象的序列化实现保存应用程序的当前工作状态,下次再启动的时候将自动地恢复到上次执行的状态。

Java 序列化技术可以使你将一个对象的状态写入一个Byte 流里,并且可以从其它地方把该Byte 流里的数据读出来,重新构造一个相同的对象。这种机制允许你将对象通过网络进行传播,并可以随时把对象持久化到数据库、文件等系统里。 Java的序列化机制是RMI、EJB等技术的技术基础。

2.序列化的特点

  • 如果某个类能够被序列化,其子类也可以被序列化。如果该类有父类,则分两种情况来考虑:如果该父类已经实现了可序列化接口。则其父类的相应字段及属性的处理和该类相同;如果该类的父类没有实现可序列化接口,则该类的父类所有的字段属性将不会序列化。对于这种父类非序列化而子类可序列化的类,子类应该自己对超类的public以及protected 属性进行单独处理。

  • 对于父类的处理,如果父类没有实现序列化接口,则其必须有默认的构造函数(即没有参数的构造函数),否则编译的时候就会报错。在反序列化的时候,默认构造函数会被调用。但是若把父类标记为可以序列化,则在反序列化的时候,其默认构造函数不会被调用。这是因为Java 对序列化的对象进行反序列化的时候,直接从流里获取其对象数据来生成一个对象实例,而不是通过其构造函数来完成。

  • 声明为static和transient类型的成员数据不能被序列化。因为对象序列化保存的是对象的”状态”,即它的成员变量;transient代表对象的临时数据。序列化保存的内容:
    1)对象的类型
    2)对象属性的类型
    3)对象属性的值
    并没有什么方法签名的信息,更不要说什么序列化方法了。

3.相关的类和接口

在java.io包中提供的涉及对象的序列化的类与接口有ObjectOutput接口、ObjectOutputStream类、ObjectInput接口、ObjectInputStream类。

ObjectOutput接口:它继承DataOutput接口并且支持对象的序列化,其内的writeObject()方法实现存储一个对象。

ObjectInput接口:它继承DataInput接口并且支持对象的序列化,其内的readObject()方法实现读取一个对象。

ObjectOutputStream类:它继承OutputStream类并且实现ObjectOutput接口。利用该类来实现将对象存储(调用ObjectOutput接口中的writeObject()方法)。

ObjectInputStream类:它继承InputStream类并且实现ObjectInput接口。利用该类来实现读取一个对象(调用ObjectInput接口中的readObject()方法)。

序列化的实现:将需要被序列化的类实现Serializable接口,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。

例:

import java.io.*;

public class Cat implements Serializable {

      private String name;

      public Cat () {
          this.name = "new cat";
      }

      public String getName() {
          return this.name;
      }

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

      public static void main(String[] args) {
          Cat cat = new Cat();
          try {
             FileOutputStream fos = new FileOutputStream("catDemo.out");
             ObjectOutputStream oos = new ObjectOutputStream(fos);
             System.out.println(" 1> " + cat.getName());
             cat.setName("My Cat");
             oos.writeObject(cat);
             oos.close();
          } catch (Exception ex) { ex.printStackTrace(); }

          try {
             FileInputStream fis = new FileInputStream("catDemo.out");
             ObjectInputStream ois = new ObjectInputStream(fis);
             cat = (Cat) ois.readObject();//可以不强制转换
             System.out.println(" 2> " + cat.getName());
             ois.close();
           } catch (Exception ex) {ex.printStackTrace();}

      }

}//writeObject和readObject本身就是线程安全的,传输过程中是不允许被并发访问的。所以对象能一个一个接连不断地传过来。

当Cat对象被保存到catDemo.out文件中之后,我们可以在其它地方去读取该文件以还原对象,但必须确保该读取程序的CLASSPATH中包含有Cat.class(哪怕在读取Cat对象时并没有显式地使用Cat类),否则会抛出ClassNotFoundException。

4.Serializable的作用

为什么一个类实现了Serializable接口,它就可以被序列化呢?java.io.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。

5.默认序列化机制

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

6.影响序列化

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

6.1 transient关键字

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

public enum Gender {
    MALE, FEMALE
}
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 + "]";
    }
}
public class SimpleSerial {

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

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

        ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
        Object newPerson = oin.readObject(); // 没有强制转换到Person类型
        oin.close();
        System.out.println(newPerson);
    }
}

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

arg constructor
[John, null, MALE]

可见,age字段未被序列化。

还需要注意的是,当重新读取被保存的Person对象时,并没有调用Person的任何构造器,看起来就像是直接使用字节将Person对象还原出来的。

6.2 writeObject()方法与readObject()方法

对于上述已被声明为transient的字段age,除了将transient关键字去掉之外,是否还有其它方法能使它再次可被序列化?方法之一就是在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()方法,该方法会执行默认的序列化机制,如6.1节所述,此时会忽略掉age字段。然后再调用writeInt()方法显式地将age字段写入到ObjectOutputStream中。readObject()的作用则是针对对象的读取,其原理与writeObject()方法相同。

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

arg constructor
[John, 31, MALE]

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

6.3 Externalizable接口

无论是使用transient关键字,还是使用writeObject()和readObject()方法,其实都是基于Serializable接口的序列化。JDK中提供了另一个序列化接口–Externalizable,使用该接口之后,之前基于Serializable接口的序列化机制就将失效。此时将Person类修改成如下:

public class Person implements Externalizable {

    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;
    }

    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();
    }

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

    }

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

    }

}

此时再执行SimpleSerial程序之后会得到如下结果:

arg constructor
none-arg constructor
[null, null, null]

从该结果,一方面可以看出Person对象中任何一个字段都没有被序列化。另一方面,如果细心的话,还可以发现这此次序列化过程调用了Person类的无参构造器。

Externalizable继承于Serializable,当使用该接口时,序列化的细节需要由程序员去完成。如上所示的代码,由于writeExternal()与readExternal()方法未作任何处理,那么该序列化行为将不会保存/读取任何一个字段。这也就是为什么输出结果中所有字段的值均为空。

另外,若使用Externalizable进行序列化,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。这就是为什么在此次序列化过程中Person类的无参构造器会被调用。由于这个原因,实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public。

对上述Person类作进一步的修改,使其能够对name与age字段进行序列化,但要忽略掉gender字段,如下代码所示:

public class Person implements Externalizable {

    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;
    }

    //下面这两个方法不需要了吧...
    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();
    }

    @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();
    }

}

执行SimpleSerial之后会有如下结果:

arg constructor
none-arg constructor
[John, 31, null]

6.4 writeReplace()与readResolve()

当我们使用Singleton模式时,应该是期望某个类的实例是唯一的,但如果该类是可序列化的,那么情况可能会略有不同。此时对第2节使用的Person类进行修改,使其实现Singleton模式,如下所示:

public class Person implements Serializable {

    private static class InstanceHolder {
        private static final Person instatnce = new Person("John", 31, Gender.MALE);
    }

    public static Person getInstance() {
        return InstanceHolder.instatnce;
    }

    private String name = null;

    private Integer age = null;

    private Gender gender = null;

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

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

}

同时要修改SimpleSerial应用,使得能够保存/获取上述单例对象,并进行对象相等性比较,如下代码所示:

public class SimpleSerial {

    public static void main(String[] args) throws Exception {
        File file = new File("person.out");
        ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
        oout.writeObject(Person.getInstance()); // 保存单例对象
        oout.close();

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

        System.out.println(Person.getInstance() == newPerson); // 将获取的对象与Person类中的单例对象进行相等性比较
    }
}

执行上述应用程序后会得到如下结果:

arg constructor
[John, 31, MALE]
false

值得注意的是,从文件person.out中获取的Person对象与Person类中的单例对象并不相等。为了能在序列化过程仍能保持单例的特性,可以在Person类中添加一个readResolve()方法,在该方法中直接返回Person的单例对象,如下所示:

public class Person implements Serializable {

    private static class InstanceHolder {
        private static final Person instatnce = new Person("John", 31, Gender.MALE);
    }

    public static Person getInstance() {
        return InstanceHolder.instatnce;
    }

    private String name = null;

    private Integer age = null;

    private Gender gender = null;

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

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

    private Object readResolve() throws ObjectStreamException {
        return InstanceHolder.instatnce;
    }

}

再次执行本节的SimpleSerial应用后将有如下输出:

arg constructor
[John, 31, MALE]
true

无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象,而被创建的对象则会被垃圾回收掉。(这种情况序列化还有必要么…)

对于实现 Serializable 或 Externalizable 接口的类来说,writeReplace() 方法可以使对象被写入流以前,用一个对象来替换自己。当序列化时,可序列化的类要将对象写入流,如果我们想要另一个对象来替换当前对象来写入流,则可以实现下面这个方法,方法的签名也要完全一致:

ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;

writeReplace()方法在 ObjectOutputStream 准备将对象写入流以前调用, ObjectOutputStream 会首先检查序列化的类是否定义了 writeReplace()方法,如果定义了这个方法,则会通过调用它,用另一个对象替换它写入流中。方法返回的对象要么与它替换的对象类型相同,要么与其兼容,否则,会抛出 ClassCastException 。

同理,当反序列化时,要将一个对象从流中读出来,我们如果想将读出来的对象用另一个对象实例替换,则要实现跟下面的方法的签名完全一致的方法:

ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

readResolve 方法在对象从流中读取出来的时候调用, ObjectInputStream 会检查反序列化的对象是否已经定义了这个方法,如果定义了,则读出来的对象返回一个替代对象。同 writeReplace()方法,返回的对象也必须是与它替换的对象兼容,否则抛出 ClassCastException。如果序列化的类中有这些方法,那么它们的执行顺序是这样的:

a. writeReplace()
b. writeObject()
c. readObject()
d. readResolve()

下面是 java doc 中关于 readResolve() 与 writeReplace()方法的英文描述:

Serializable classes that need to designate an alternative object to be used when writing an object to the stream should implement this special method with the exact signature:

ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;

Classes that need to designate a replacement when an instance of it is read from the stream should implement this special method with the exact signature:

ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

6.5 可序列化类的不同版本的序列化兼容性

凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:

private static final long serialVersionUID;

以上serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。如果对类的源代码作了修改,再重新编译,新生成的类文件的serialVersionUID的取值有可能会发生变化。

类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的serialVersionUID,也有可能相同。为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显式地定义serialVersionUID,为它赋予明确的值。显式地定义serialVersionUID有两种用途:

  • 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;

  • 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

7.谨慎地实现序列化

一般认为只声明实现 implements 接口, 不提供自定义的序列化形式是不负责任的做法, 这样可能导致比较多的问题比如类的不同版本之间的兼容性, 看看 Effective Java 中的条目吧:

  • 为了继承而设计的类应该很少实现 Serialiable, 接口也应该很少会扩展它. 如果违反了这条规则, 则扩展这个类或者实现这个接口的程序员会背上沉重的负担。

  • 若没有认真考虑默认序列化形式是否合适, 则不要接受这种形式。

  • 即使你确定了默认序列化形式是合适的, 通常你仍然要提供一个 readObject方法以保证约束关系和约束性。

  • 不管你选择了哪种序列化形式,你都要为自己编写的每个可序列化的类声明一个显式的序列版本 UID (serialVersionUID)。

参考:

http://www.blogjava.net/jiangshachina/archive/2012/02/13/369898.html

http://blog.sina.com.cn/s/blog_4e345ce70100rt86.html

http://blog.csdn.net/yakihappy/article/details/3979373

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值