Java序列化

前些天看 sun 关于 java.io.Serializable 接口的文档,居然半天都没有看懂,感慨于自己的E文越来越没有水准的同时,又仔细翻了一些资料,终于有点眉目了,便整理出来,以资记录。

一、什么是序列化?

“对象序列化”(Object Serialization)是 Java1.1就开始有的特性。 简单地说,就是可以将一个对象及其状态转换为字节码,保存起来(可以保存在数据库,内存,文件等),然后可以在适当的时候再将其状态恢复(也就是反序列化)。serialization 不但可以在本机做,而且可以经由网络操作。它自动屏蔽了操作系统的差异,字节顺序等。比如,在 Windows 平台生成一个对象并序列化之,然后通过网络传到一台 Unix 机器上,然后可以在这台Unix机器上正确地重构(deserialization)这个对象。 不必关心数据在不同机器上如何表示,也不必关心字节的顺序或者其他任何细节。

利用 Java 的这种特性可以实现“有限持久化”。“持久化”意味着对象的“生存时间”并不取决于程序是否正在执行——它存在或“生存”于程序的每一次调用之间。通过序列化一个对象,将其写入磁盘,以后在程序重新调用时重新恢复那个对象,就能圆满实现一种“持久”的效果。之所以称其为“有限”,是因为不能用某种“persistent”(持久)关键字简单地地定义一个对象,并让系统自动照看其他所有细节问题(尽管将来可能成为现实)。相反,必须在自己的程序中明确地序列化和组装对象。

另外,还应明白以下几点:

a. java.io.Serializable接口没有任何方法属性域,实现它的类只是从语义上表明自己是可以序列化的。

b. 在对一个 Serializable(可序列化)对象进行重新装配的过程中,不会调用任何构建器(甚至默认构建器)。整个对象都是通过从 InputStream 中取得数据恢复的。

c. 如是要一个类是可序列化的,那么它的子类也是可序列化的。


二、序列化在什么时候用?

可提供对 Java 两种主要特性的支持:

远程方法调用(RMI):使本来存在于其他机器的对象可以表现出好象就在本地机器上的行为。将消息发给远程对象时,需要通过对象序列化来传输参数和返回值。

对象的序列化也是 Java Beans 必需的。使用一个 Bean 时,它的状态信息通常在设计期间配置好。程序启动以后,这种状态信息必须保存下来,以便程序启动以后恢复;具体工作由对象序列化完成。

三、如何序列化

在Java里,如果要使一个类可以序列化或反序列化,只需要实现 java.io.Serializable 接口。如果类没有实现这个接口,则一般来说不能将他们的状态进行序列化与反序列化。注意,这里我说"一般来说",是因为Java还提供了另外一个接口 java.io.Externalizable,关于这个接口的使用我将会在下面单独说明。

四、特殊情况-父类不可序列化而子类可序列化

我们假设这样一种情况,如果一个类是不可序列化的,但我们新建了一个类,这个类是前一个不可序列化的类的子类,那么从这个不可序列化的类继承过来的属性是否可以序列化呢?我们可以先看一下下面的例子:

//class TestSerializable.java
public class TestSerializable {
 public static void main(String[] args) throws IOException,ClassNotFoundException  {
  FileOutputStream fos = new FileOutputStream("c:/a.txt");
  ObjectOutputStream oos = new ObjectOutputStream(fos);
  B b = new B();
  oos.writeObject(b);
  oos.close();
  System.out.println("before write object=" + b);
  
  System.out.println("***********************");
  
  FileInputStream fis = new FileInputStream("c:/a.txt");
  ObjectInputStream ois = new ObjectInputStream(fis);
  b = (B)ois.readObject();
  ois.close();
  System.out.println("after read object=" +b);
 
 }
}

class A {
 public A(){
  System.out.println("A's Constructor");
  a="a1";
  b="b1";
 }
 String a = "a";
 String b = "b";
}

class B extends A implements Serializable {
        String b1 = "b1";
        public B(){
            a = "a2";
            b = "b2";
         System.out.println("B's constructor");
        }
       
        public String toString() {
         return "b1=" + b1 + " a=" + a + " b=" + b;
        }

}

运行结果如下:
        A's Constructor
        B's constructor
        before write object=b1=b1 a=a2 b=b2
        ***********************
        A's Constructor
        after read object=b1=b1 a=a1 b=b1

我们看到,对于B的属性b1,其可以正确的序列化,但对于其从A继承过来的属性a,b则没有正确的序列化。为什么呢?我们再看上面的运行结果,可以发现:反序列化的时候,由于B实现了Serializable,所在以反序列化的时候,它并不会调用它自己的构造器,但是,在反序列化B的时候,却调用了它的超类的构造器(实际上不仅仅是构造器,A的所有的初始化过程都会正常进行)。这正是上面结果中a,b的值没有正确反序列化的原因。

于是,我们得出,对于这种父类不是序列化而子类可以序列化的类,子类应该自己对超类的public,protected,以及 friedly 属性进行单独处理。

五、序列化的控制

正如大家看到的那样,默认的序列化机制并不难操纵。然而,假若有特殊要求又该怎么办呢?我们可能有特殊的安全问题,不希望对象的某一部分序列化;或者某一个子对象完全不必序列化。通常,我们可以采用以下几种方法:

1. 实现Externalizable接口,用它代替Serializable接口,便可控制序列化的具体过程。这个Externalizable接口扩展了Serializable,并增添了两个方法:writeExternal()和readExternal()。在序列化和重新装配的过程中,会自动调用这两个方法,以便我们执行一些特殊操作。如果实现了这个接口,则所有的序列化与反序列化,都必须由我们自己来控制。

下面这个例子展示了Externalizable接口方法的简单应用。注意Blip1和Blip2几乎完全一致,除了极微小的差别(自己研究一下代码,看看是否能发现):

//: Blips.java
// Simple use of Externalizable & a pitfall
import java.io.*;
import java.util.*;

class Blip1 implements Externalizable {
  public Blip1() {
    System.out.println("Blip1 Constructor");
  }
  public void writeExternal(ObjectOutput out)
      throws IOException {
    System.out.println("Blip1.writeExternal");
  }
  public void readExternal(ObjectInput in)
     throws IOException, ClassNotFoundException {
    System.out.println("Blip1.readExternal");
  }
}

class Blip2 implements Externalizable {
  Blip2() {
    System.out.println("Blip2 Constructor");
  }
  public void writeExternal(ObjectOutput out)
      throws IOException {
    System.out.println("Blip2.writeExternal");
  }
  public void readExternal(ObjectInput in)
     throws IOException, ClassNotFoundException {
    System.out.println("Blip2.readExternal");
  }
}

public class Blips {
  public static void main(String[] args) {
    System.out.println("Constructing objects:");
    Blip1 b1 = new Blip1();
    Blip2 b2 = new Blip2();
    try {
      ObjectOutputStream o =
        new ObjectOutputStream(
          new FileOutputStream("Blips.out"));
      System.out.println("Saving objects:");
      o.writeObject(b1);
      o.writeObject(b2);
      o.close();
      // Now get them back:
      ObjectInputStream in =
        new ObjectInputStream(
          new FileInputStream("Blips.out"));
      System.out.println("Recovering b1:");
      b1 = (Blip1)in.readObject();
      // OOPS! Throws an exception:
//!   System.out.println("Recovering b2:");
//!   b2 = (Blip2)in.readObject();
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

该程序输出如下:

Constructing objects:
Blip1 Constructor
Blip2 Constructor
Saving objects:
Blip1.writeExternal
Blip2.writeExternal
Recovering b1:
Blip1 Constructor
Blip1.readExternal

未恢复Blip2对象的原因是那样做会导致一个违例。你找出了Blip1和Blip2之间的区别吗?Blip1的构建器是“公共的”(public),Blip2的构建器则不然,这样便会在恢复时造成违例。试试将Blip2的构建器属性变成“public”,然后删除//!注释标记,看看是否能得到正确的结果。
恢复b1后,会调用Blip1默认构建器。这与恢复一个Serializable(可序列化)对象不同。在后者的情况下,对象完全以它保存下来的二进制位为基础恢复,不存在构建器调用。而对一个Externalizable对象,所有普通的默认构建行为都会发生(包括在字段定义时的初始化),而且会调用readExternal()。必须注意这一事实——特别注意所有默认的构建行为都会进行——否则很难在自己的Externalizable对象中产生正确的行为。
下面这个例子揭示了保存和恢复一个Externalizable对象必须做的全部事情:

//: Blip3.java
// Reconstructing an externalizable object
import java.io.*;
import java.util.*;

class Blip3 implements Externalizable {
  int i;
  String s; // No initialization
  public Blip3() {
    System.out.println("Blip3 Constructor");
    // s, i not initialized
  }
  public Blip3(String x, int a) {
    System.out.println("Blip3(String x, int a)");
    s = x;
    i = a;
    // s & i initialized only in non-default
    // constructor.
  }
  public String toString() { return s + i; }
  public void writeExternal(ObjectOutput out)
      throws IOException {
    System.out.println("Blip3.writeExternal");
    // You must do this:
    out.writeObject(s);
    out.writeInt(i);
  }
  public void readExternal(ObjectInput in)
     throws IOException, ClassNotFoundException {
    System.out.println("Blip3.readExternal");
    // You must do this:
    s = (String)in.readObject();
    i =in.readInt();
  }
  public static void main(String[] args) {
    System.out.println("Constructing objects:");
    Blip3 b3 = new Blip3("A String ", 47);
    System.out.println(b3.toString());
    try {
      ObjectOutputStream o =
        new ObjectOutputStream(
          new FileOutputStream("Blip3.out"));
      System.out.println("Saving object:");
      o.writeObject(b3);
      o.close();
      // Now get it back:
      ObjectInputStream in =
        new ObjectInputStream(
          new FileInputStream("Blip3.out"));
      System.out.println("Recovering b3:");
      b3 = (Blip3)in.readObject();
      System.out.println(b3.toString());
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

其中,字段s和i只在第二个构建器中初始化,不关默认构建器的事。这意味着假如不在readExternal中初始化s和i,它们就会成为null。若注释掉跟随于“You must do this”后面的两行代码,并运行程序,就会发现当对象恢复以后,s是null,而i是零。
若从一个Externalizable对象继承,通常需要调用writeExternal()和readExternal()的基础类版本,以便正确地保存和恢复基础类组件。
所以为了让一切正常运作起来,千万不可仅在writeExternal()方法执行期间写入对象的重要数据(没有默认的行为可用来为一个Externalizable对象写入所有成员对象)的,而是必须在readExternal()方法中也恢复那些数据。初次操作时可能会有些不习惯,因为Externalizable对象的默认构建行为使其看起来似乎正在进行某种存储与恢复操作。但实情并非如此。

2. Externalizable的替代方法

若不是特别在意要实现Externalizable接口,还有另一种方法可供选用。我们可以实现Serializable接口,并添加(注意是“添加”,而非“覆盖”或者“实现”)名为writeObject()和readObject()的方法。一旦对象被序列化或者重新装配,就会分别调用那两个方法。也就是说,只要提供了这两个方法,就会优先使用它们,而不考虑默认的序列化机制。

这些方法必须含有下列准确的签名:

private void
  writeObject(ObjectOutputStream stream)
    throws IOException;

private void
  readObject(ObjectInputStream stream)
    throws IOException, ClassNotFoundException

从设计的角度出发,情况变得有些扑朔迷离。首先,大家可能认为这些方法不属于基础类或者Serializable接口的一部分,它们应该在自己的接口中得到定义。但请注意它们被定义成“private”,这意味着它们只能由这个类的其他成员调用。然而,我们实际并不从这个类的其他成员中调用它们,而是由 ObjectOutputStream 和 ObjectInputStream 的 writeObject() 及 readObject() 方法来调用我们对象的 writeObject() 和 readObject() 方法(注意我在这里用了很大的抑制力来避免使用相同的方法名——因为怕混淆)。大家可能奇怪 ObjectOutputStream 和 ObjectInputStream 如何有权访问我们的类的 private 方法——只能认为这是序列化机制玩的一个把戏。

在任何情况下,接口中的定义的任何东西都会自动具有public属性,所以假若 writeObject() 和 readObject() 必须为private,那么它们不能成为接口(interface)的一部分。但由于我们准确地加上了签名,所以最终的效果实际与实现一个接口是相同的。

看起来似乎我们调用 ObjectOutputStream.writeObject() 的时候,我们传递给它的 Serializable 对象似乎会被检查是否实现了自己的 writeObject()(不是检查它的接口,它根本就没有,也不是检查类的类型,而是利用反射方法实际搜索方法)。若答案是肯定的是,便会跳过常规的序列化过程,并调用 writeObject(),类似的情况也会在readObject()上发生。

writeObject 负责保存对象的状态,而readObject负责将保存的状态恢复。对于那些可以使用缺省的序列化机制处理的状态属性,可以调用 out.defaultWriteObject() 及 in.defaultReadObject()来使系统自己处理。

3. transient(临时)关键字

控制序列化过程时,可能有一个特定的子对象不愿让Java的序列化机制自动保存与恢复。一般地,若那个子对象包含了不想序列化的敏感信息(如密码),就会面临这种情况。即使那种信息在对象中具有“private”(私有)属性,但一旦经序列化处理,人们就可以通过读取一个文件,或者拦截网络传输得到它。

为防止对象的敏感部分被序列化,一个办法是将自己的类实现为Externalizable,这样一来,没有任何东西可以自动序列化,只能在 writeExternal() 明确序列化那些需要的部分。

然而,若操作的是一个 Serializable 对象,所有序列化操作都会自动进行。为解决这个问题,可以用transient(临时)逐个字段地关闭序列化。

例如,假设一个Login对象包含了与一个特定的登录会话有关的信息。校验登录的合法性时,一般都想将数据保存下来,但不包括密码。为做到这一点,最简单的办法是实现 Serializable,并将 password 字段设为 transient。下面是具体的代码:

//: Logon.java
// Demonstrates the "transient" keyword
import java.io.*;
import java.util.*;

class Logon implements Serializable {
  private Date date = new Date();
  private String username;
  private transient String password;
  Logon(String name, String pwd) {
    username = name;
    password = pwd;
  }
  public String toString() {
    String pwd =
      (password == null) ? "(n/a)" : password;
    return "logon info: /n   " +
      "username: " + username +
      "/n   date: " + date.toString() +
      "/n   password: " + pwd;
  }
  public static void main(String[] args) {
    Logon a = new Logon("Hulk", "myLittlePony");
    System.out.println( "logon a = " + a);
    try {
      ObjectOutputStream o =
        new ObjectOutputStream(
          new FileOutputStream("Logon.out"));
      o.writeObject(a);
      o.close();
      // Delay:
      int seconds = 5;
      long t = System.currentTimeMillis()
             + seconds * 1000;
      while(System.currentTimeMillis() < t)
        ;
      // Now get them back:
      ObjectInputStream in =
        new ObjectInputStream(
          new FileInputStream("Logon.out"));
      System.out.println(
        "Recovering object at " + new Date());
      a = (Logon)in.readObject();
      System.out.println( "logon a = " + a);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

可以看到,其中的date和username字段保持原始状态(未设成transient),所以会自动序列化。然而,password被设为transient,所以不会自动保存到磁盘;另外,自动序列化机制也不会作恢复它的尝试。输出如下:

logon a = logon info:
   username: Hulk
   date: Sun Mar 23 18:25:53 PST 1997
   password: myLittlePony
Recovering object at Sun Mar 23 18:25:59 PST 1997
logon a = logon info:
   username: Hulk
   date: Sun Mar 23 18:25:53 PST 1997
   password: (n/a)

由于Externalizable对象默认时不保存它的任何字段,所以transient关键字只能伴随Serializable使用。

六、关于 writeReplace()与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;
 This writeReplace method is invoked by serialization if the method exists and it would be accessible from a method defined within the class of the object being serialized. Thus, the method can have private, protected and package-private access. Subclass access to this method follows java accessibility rules.

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;
 This readResolve method follows the same invocation rules and accessibility rules as writeReplace.

七、序列化版本ID serialVersionUID

    private static final long serialVersionUID = xxxxxxxxxxxxxxL;

    版本号是一个64位的 hash 码,如果 SUID 没有被声明,则会生成一个缺省值。Serializable 的类不一定要有 SUID,但 Externalizable 的类一定要有。
   
    SUID 主要是为了当前类是否与初始版本不同。

    对于作为内部类的 serializable 类或包含内部类的 Serializable 类,最好声明一个SUID,因为缺省生成的 SUID 跟类名及字段名等有关,但对于不同的编译器,生在的内部类的类名不同,这样生成的 SUID 也不同。



Trackback: http://tb.donews.net/TrackBack.aspx?PostId=484308

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值