`

对象序列化

 
阅读更多

当你创建对象时,只要你需要,它就会一直存在,但是在程序终止时,无论如何它都不会继续存在。如果对象能

够在程序不运行的情况下仍然存在并保存其信息,那将非常有用。这样,在下一次运行程序时,该对象将被重建

并且拥有的信息与在程序上次运行时它所拥有的信息相同。当然,可以通过将对象信息写入文件或是数据库来达

到相同的效果,但是在使万物都成为对象的精神中,如果能够将一个对象声明为是持久性的,并为我们处理掉所

有细节,那将会显得非常方便。

java的对象序列化将那些实现了Serializable接口的对象转换成为一个字节学列,并能够在以后将这个字节序列

完全恢复为原来的样子。这一过程甚至可以通过网络进行,可以在windows上创建对象并将其序列化,然后通过网

络传递给unix机子,然后在那里重新组装该对象。


对象的序列化处理非常简单,只需对象实现了Serializable接口即可(该接口仅是一个标记,没有方法)。在

Java1.1中,许多标准库类都发生了改变,以便能够序列化——其中包括用于基本数据类型的全部封装器、所有集

合类以及其他许多东西。甚至Class对象也可以序列化

为序列化一个对象,首先要创建某些OutputStream对象,然后将其封装到ObjectOutputStream对象内。此时,只

需调用 writeObject()即可完成对象的序列化,并将其发送给OutputStream。相反的过程是将一个InputStream封

装到 ObjectInputStream内,然后调用readObject()。和往常一样,我们最后获得的是指向一个上溯造型Object

的句柄,所以必须下溯造型,以便能够直接设置。

InputStream在read的时候,都是即时读取的,执行完马上就执行IO操作并返回数据。
而OutputStream为了提高效率都不是即时输出的,write会把数据先写到缓存里面,积累够了再一次性做IO输出,

所以你虽然write了,但数据还没传出去,flush方法就是强制对buffer里面的内容做IO输出。

java的io流相当于一个数据流通的管道,这里面存在一个默认的数据缓冲区,大小是8k。当我们在进行io操作的

时候,数据会经过这个缓冲区,然后保存到硬盘文件或其它存储设备文件中。如果保存过程中的数据少于8k之后,

那么这时,它就有可能把少于8k的这部分数据缓存在缓冲区中,而不写进存储设备里面,因此,我们调用当前io

流的flush()方法,清空当前缓冲区,即把数据完整的存储到文件中。


对象序列化特别“聪明”的一个地方是它不仅保存了对象的“全景图”,而且能追踪对象内包含的所有句柄并保

存那些对象;接着又能对每个对象内包含的句柄进行追踪;以此类推。我们有时将这种情况称为“对象网”,单

个对象可与之建立连接。而且它还包含了对象的句柄数组以及成员对象。若必须自行操纵一套对象序列化机制,

那么在代码里追踪所有这些链接时可能会显得非常麻烦。在另一方面,由于Java对象的序列化似乎找不出什么缺

点,所以请尽量不要自己动手,让它用优化的算法自动维护整个对象网。下面这个例子对序列化机制进行了测试

。它建立了许多链接对象的一个“Worm”(蠕虫),每个对象都与Worm中的下一段链接,同时又与属于不同类

(Data)的对象句柄数组链接:


import java.io.*;
import java.util.*;

class Data implements Serializable {
    private int n;

    public Data(int n) {
        this.n = n;
    }

    public String toString() {
        return Integer.toString(n);
    }
}

public class Worm implements Serializable {
    private static Random rand = new Random(47);
    private Data[] d = { new Data(rand.nextInt(10)),
            new Data(rand.nextInt(10)), new Data(rand.nextInt(10)) };
    private Worm next;
    private char c;

    // Value of i == number of segments
    public Worm(int i, char x) {
        System.out.println("Worm constructor: " + i);
        c = x;
        if (--i > 0)
            next = new Worm(i, (char) (x + 1));
    }

    public Worm() {
        System.out.println("Default constructor");
    }

    public String toString() {
        StringBuilder result = new StringBuilder(":");
        result.append(c);
        result.append("(");
        for (Data dat : d)
            result.append(dat);
        result.append(")");
        if (next != null)
            result.append(next);
        return result.toString();
    }

    public static void main(String[] args) throws ClassNotFoundException,
            IOException {
        Worm w = new Worm(6, 'a');
        System.out.println("w = " + w);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(
                "worm.out"));
        out.writeObject("Worm storage\n");
        out.writeObject(w);
        out.close(); // Also flushes output
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(
                "worm.out"));
        String s = (String) in.readObject();
        Worm w2 = (Worm) in.readObject();
        System.out.println(s + "w2 = " + w2);
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream out2 = new ObjectOutputStream(bout);
        out2.writeObject("Worm storage\n");
        out2.writeObject(w);
        out2.flush();
        ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(
                bout.toByteArray()));
        s = (String) in2.readObject();
        Worm w3 = (Worm) in2.readObject();
        System.out.println(s + "w3 = " + w3);
    }
}
    更有趣的是,Worm内的Data对象数组是用随机数字初始化的(这样便不用怀疑编译器保留了某种原始信息)

。每个Worm段都用一个Char标记。这个 Char是在重复生成链接的Worm列表时自动产生的。创建一个Worm时,需告

诉构建器希望它有多长。为产生下一个句柄(next),它总是用减去1的长度来调用Worm构建器。最后一个next句

柄则保持为null(空),表示已抵达Worm的尾部。
    上面的所有操作都是为了加深事情的复杂程度,加大对象序列化的难度。然而,真正的序列化过程却是非常

简单的。一旦从另外某个流里创建了 ObjectOutputStream,writeObject()就会序列化对象。注意也可以为一个

String调用writeObject()。亦可使用与DataOutputStream相同的方法写入所有基本数据类型(它们有相同的接口

)。
    有两个单独的try块看起来是类似的。第一个读写的是文件,而另一个读写的是一个ByteArray(字节数组)

。可利用对任何DataInputStream或者DataOutputStream的序列化来读写特定的对象;正如在关于连网的那一章会

讲到的那样,这些对象甚至包括网络。

    可以看出,装配回原状的对象确实包含了原来那个对象里包含的所有链接。注意在对一个Serializable(可

序列化)对象进行重新装配的过程中,不会调用任何构建器(甚至默认构建器)。整个对象都是通过从

InputStream中取得数据恢复的。











该程序能打开文件,并成功读取mystery对象中的内容。然而,一旦尝试查找与对象有关的任何资料——这要求

Alien的Class对象——Java虚拟机(JVM)便找不到Alien.class(除非它正好在类路径内,而本例理应相反)。

这样就会得到一个名叫 ClassNotFoundException的违例(同样地,若非能够校验Alien存在的证据,否则它等于

消失)。
恢复了一个序列化的对象后,如果想对其做更多的事情,必须保证JVM能在本地类路径或者因特网的其他什么地方

找到相关的.class文件。
例如:

public class ThawAlien {
    public static void main(String[] args) throws Exception {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(
                new File("..", "X.file")));
        Object mystery = in.readObject();
        System.out.println(mystery.getClass());
    }
}
正如大家看到的那样,默认的序列化机制并不难操纵。然而,假若有特殊要求又该怎么办呢?我们可能有特殊的

安全问题,不希望对象的某一部分序列化;或者某一个子对象完全不必序列化,因为对象恢复以后,那一部分需

要重新创建。
此时,通过实现Externalizable接口,用它代替Serializable接口,便可控制序列化的具体过程。这个

Externalizable接口扩展了Serializable,并增添了两个方法:writeExternal()和readExternal()。在序列化和

重新装配的过程中,会自动调用这两个方法,以便我们执行一些特殊操作。
下面这个例子展示了Externalizable接口方法的简单应用。注意Blip1和Blip2几乎完全一致,除了极微小的差别

(自己研究一下代码,看看是否能发现):

import java.io.Externalizable;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

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) throws IOException,
            ClassNotFoundException {
        System.out.println("Constructing objects:");
        Blip1 b1 = new Blip1();
        Blip2 b2 = new Blip2();
        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();
    }
}

未恢复Blip2对象的原因是那样做会导致一个违例。你找出了Blip1和Blip2之间的区别吗?Blip1的构建器是“公

共的” (public),Blip2的构建器则不然,这样便会在恢复时造成违例。试试将Blip2的构建器属性变成

“public”,然后删除//!注释标记,看看是否能得到正确的结果。
恢复b1后,会调用Blip1默认构建器。这与恢复一个Serializable(可序列化)对象不同。在后者的情况下,对象

完全以它保存下来的二进制位为基础恢复,不存在构建器调用。而对一个Externalizable对象,所有普通的默认

构建行为都会发生(包括在字段定义时的初始化),而且会调用readExternal()。必须注意这一事实——特别注

意所有默认的构建行为都会进行——否则很难在自己的 Externalizable对象中产生正确的行为。
下面这个例子揭示了保存和恢复一个Externalizable对象必须做的全部事情:

 

 

 
















import java.io.Externalizable;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class Blip3 implements Externalizable {
    private int i;
    private 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) throws IOException,
            ClassNotFoundException {
        System.out.println("Constructing objects:");
        Blip3 b3 = new Blip3("A String ", 47);
        System.out.println(b3);
        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);
    }
}
其中,字段s和i只在第二个构建器中初始化,不关默认构建器的事。这意味着假如不在readExternal中初始化s和

i,它们就会成为null(因为在对象创建的第一步中已将对象的存储空间清除为1)。若注释掉跟随于

“Youmustdothis”后面的两行代码,并运行程序,就会发现当对象恢复以后,s是null,而i是零。
若从一个Externalizable对象继承,通常需要调用writeExternal()和readExternal()的基础类版本,以便正确地

保存和恢复基础类组件。
所以为了让一切正常运作起来,千万不可仅在writeExternal()方法执行期间写入对象的重要数据(没有默认的行

为可用来为一个 Externalizable对象写入所有成员对象)的,而是必须在readExternal()方法中也恢复那些数据

。初次操作时可能会有些不习惯,因为Externalizable对象的默认构建行为使其看起来似乎正在进行某种存储与

恢复操作。但实情并非如此。




1.transient(临时)关键字
控制序列化过程时,可能有一个特定的子对象不愿让Java的序列化机制自动保存与恢复。一般地,若那个子对象

包含了不想序列化的敏感信息(如密码),就会面临这种情况。即使那种信息在对象中具有“private”(私有)

属性,但一旦经序列化处理,人们就可以通过读取一个文件,或者拦截网络传输得到它。

为防止对象的敏感部分被序列化,一个办法是将自己的类实现为Externalizable,就象前面展示的那样。这样一

来,没有任何东西可以自动序列化,只能在writeExternal()明确序列化那些需要的部分。
然而,若操作的是一个Serializable对象,所有序列化操作都会自动进行。为解决这个问题,可以用transient(

临时)逐个字段地关闭序列化,它的意思是“不要麻烦你(指自动机制)保存或恢复它了——我会自己处理的”


例如,假设一个Login对象包含了与一个特定的登录会话有关的信息。校验登录的合法性时,一般都想将数据保存

下来,但不包括密码。为做到这一点,最简单的办法是实现Serializable,并将password字段设为transie
nt。下面是具体的代码:
public class Logon implements Serializable {
    private Date date = new Date();
    private String username;
    private transient String password;

    public Logon(String name, String pwd) {
        username = name;
        password = pwd;
    }

    public String toString() {
        return "logon info: \n   username: " + username + "\n   date: " + date
                + "\n   password: " + password;
    }

    public static void main(String[] args) throws Exception {
        Logon a = new Logon("Hulk", "myLittlePony");
        System.out.println("logon a = " + a);
        ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(
                "Logon.out"));
        o.writeObject(a);
        o.close();
        TimeUnit.SECONDS.sleep(1); // Delay
        // 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);
    }
}


可以看到,其中的date和username字段保持原始状态(未设成transient),所以会自动序列化。然而,password

被设为transient,所以不会自动保存到磁盘;另外,自动序列化机制也不会作恢复它的尝试。
由于Externalizable对象默认时不保存它的任何字段,所以transient关键字只能伴随Serializable使用。


Externalizable的替代方法
若不是特别在意要实现Externalizable接口,还有另一种方法可供选用。我们可以实现 Serializable接口,并添

加(注意是“添加”,而非“覆盖”或者“实现”)名为writeObject()和readObject()的方法。一旦对象被序列

化或者重新装配,就会分别调用那两个方法。也就是说,只要提供了这两个方法,就会优先使用它们,而不考虑

默认的序列化机制。
这些方法必须含有下列准确的签名:


pirvate void writeObject(ObjectOutputStream stream)throws IOException;

pirvate 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()内部,可以调用defaultWriteObject(),从而决定采取默认的

writeObject()行动。类似地,在readObject()内部,可以调用defaultReadObject()。下面这个简单的例子演示

了如何对一个Serializable对象的存储与恢复进行控制:
public class SerialCtl implements Serializable {
  private String a;
  private transient String b;
  public SerialCtl(String aa, String bb) {
    a = "Not Transient: " + aa;
    b = "Transient: " + bb;
  }
  public String toString() { return a + "\n" + b; }
  private void writeObject(ObjectOutputStream stream)
  throws IOException {
    stream.defaultWriteObject();
    stream.writeObject(b);
  }
  private void readObject(ObjectInputStream stream)
  throws IOException, ClassNotFoundException {
    stream.defaultReadObject();
    b = (String)stream.readObject();
  }
  public static void main(String[] args)
  throws IOException, ClassNotFoundException {
    SerialCtl sc = new SerialCtl("Test1", "Test2");
    System.out.println("Before:\n" + sc);
    ByteArrayOutputStream buf= new ByteArrayOutputStream();
    ObjectOutputStream o = new ObjectOutputStream(buf);
    o.writeObject(sc);
    // Now get it back:
    ObjectInputStream in = new ObjectInputStream(
      new ByteArrayInputStream(buf.toByteArray()));
    SerialCtl sc2 = (SerialCtl)in.readObject();
    System.out.println("After:\n" + sc2);
  }
}
在这个例子中,一个String保持原始状态,其他设为transient(临时),以便证明非临时字段会被

defaultWriteObject()方法自动保存,而transient字段必须在程序中明确保存和恢复。字段是在构建器内部初始

化的,而不是在定义的时候,这证明了它们不会在重新装配的时候被某些自动化机制初始化。

若准备通过默认机制写入对象的非transient部分,那么必须调用defaultWriteObject(),令其作为writeObject

()中的第一个操作;并调用defaultReadObject(),令其作为readObject()的第一个操作。这些都是不常见的调用

方法。举个例子来说,当我们为一个ObjectOutputStream调用defaultWriteObject()的时候,而且没有为其传递

参数,就需要采取这种操作,使其知道对象的句柄以及如何写入所有非transient的部分。这种做法非常不便。
transient对象的存储与恢复采用了我们更熟悉的代码。现在考虑一下会发生一些什么事情。在main()中会创建一

个SerialCtl对象,随后会序列化到一个ObjectOutputStream里(注意这种情况下使用的是一个缓冲区,而非文件

——与ObjectOutputStream完全一致)。正式的序列化操作是在下面这行代码里发生的:
o.writeObject(sc);
其中,writeObject()方法必须核查sc,判断它是否有自己的writeObject()方法(不是检查它的接口——它根本

就没有,也不是检查类的类型,而是利用反射方法实际搜索方法)。若答案是肯定的,就使用那个方法。类似的

情况也会在readObject()上发生。或许这是解决问题唯一实际的方法,但确实显得有些古怪。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics