7

由于各种原因,我有一个自定义序列化,我将一些相当简单的对象转储到数据文件中。可能有 5-10 个类,生成的对象图是非循环的并且非常简单(每个序列化对象都有 1 或 2 个对另一个序列化对象的引用)。例如:

class Foo
{
    final private long id;
    public Foo(long id, /* other stuff */) { ... }
}

class Bar
{
    final private long id;
    final private Foo foo;
    public Bar(long id, Foo foo, /* other stuff */) { ... }
}

class Baz
{
    final private long id;
    final private List<Bar> barList;
    public Baz(long id, List<Bar> barList, /* other stuff */) { ... }
}

id 字段仅用于序列化,因此当我序列化到文件时,我可以通过记录到目前为止已序列化的 ID 来写入对象,然后为每个对象检查其子对象是否已序列化并写入那些没有的,最后通过写入其数据字段和与其子对象对应的 ID 来编写对象本身。

令我困惑的是如何分配 id。我想了想,分配ID似乎有三种情况:

  • 动态创建的对象 - id 是从递增的计数器分配的
  • 从磁盘读取对象 -- id 是从存储在磁盘文件中的数字分配的
  • 单例对象——在任何动态创建的对象之前创建对象,以表示始终存在的单例对象。

我怎样才能正确处理这些?我觉得我在重新发明轮子,必须有一种成熟的技术来处理所有的情况。


澄清:就像一些切线信息一样,我正在查看的文件格式大致如下(忽略了一些不相关的细节)。它经过优化,可以处理大量密集的二进制数据(数十/数百 MB),并能够在其中散布结构化数据。密集的二进制数据占文件大小的 99.9%。

该文件由一系列用作容器的纠错块组成。每个块可以被认为包含一个由一系列数据包组成的字节数组。可以连续读取一个数据包(例如,可以知道每个数据包的结尾在哪里,然后下一个数据包立即开始)。

因此,该文件可以被认为是存储在纠错层之上的一系列数据包。这些数据包中的绝大多数都是不透明的二进制数据,与这个问题无关。然而,这些数据包中的一小部分是包含序列化结构化数据的项目,形成了一种由数据“岛屿”组成的“群岛”,这些数据“岛屿”可以通过对象引用关系链接起来。

所以我可能有一个文件,其中数据包 2971 包含一个序列化的 Foo,数据包 12083 包含一个序列化的 Bar,它引用数据包 2971 中的 Foo。(数据包 0-2970 和 2972​​-12082 是不透明的数据包)

所有这些数据包都是不可变的(因此考虑到 Java 对象构造的约束,它们形成了一个非循环对象图),因此我不必处理可变性问题。它们也是通用Item接口的后代。我想做的是将任意Item对象写入文件。如果Item包含对文件中已经存在的其他Items 的引用,我也需要将它们写入文件,但前提是它们尚未写入。否则,当我读回它们时,我将需要以某种方式合并它们的副本。

4

3 回答 3

4

你真的需要这样做吗?在内部,ObjectOutputStream跟踪哪些对象已经被序列化。同一个对象的后续写入只存储一个内部引用(类似于只写出 id),而不是再次写出整个对象。

有关详细信息,请参阅序列化缓存。

如果 ID 对应于某些外部定义的身份,例如实体 ID,那么这是有道理的。但问题指出,生成 ID 纯粹是为了跟踪哪些对象被序列化。

您可以通过该readResolve方法处理单例。一个简单的方法是将新反序列化的实例与您的单例实例进行比较,如果匹配,则返回单例实例而不是反序列化的实例。例如

   private Object readResolve() {
      return (this.equals(SINGLETON)) ? SINGLETON : this;
      // or simply
      // return SINGLETON;
   }

编辑:针对评论,流主要是二进制数据(以优化格式存储),复杂对象分散在该数据中。这可以通过使用支持子流的流格式(例如 zip)或简单的块分块来处理。例如,流可以是一系列块:

offset 0  - block type
offset 4  - block length N
offset 8  - N bytes of data
...
offset N+8  start of next block

然后,您可以拥有二进制数据块、序列化数据块、XStream 序列化数据块等。由于每个块都知道它的大小,您可以创建一个子流以从文件中的位置读取该长度。这使您可以自由混合数据而无需担心解析。

要实现流,请让您的主流解析块,例如

   DataInputStream main = new DataInputStream(input);
   int blockType = main.readInt();
   int blockLength = main.readInt();
   // next N bytes are the data
   LimitInputStream data = new LimitInputStream(main, blockLength);

   if (blockType==BINARY) {
      handleBinaryBlock(new DataInputStream(data));
   }
   else if (blockType==OBJECTSTREAM) {
      deserialize(new ObjectInputStream(data));
   }
   else
      ...

LimitInputStream看起来像这样的草图:

public class LimitInputStream extends FilterInputStream
{
   private int bytesRead;
   private int limit;
   /** Reads up to limit bytes from in */
   public LimitInputStream(InputStream in, int limit) {
      super(in);
      this.limit = limit;
   }

   public int read(byte[] data, int offs, int len) throws IOException {
      if (len==0) return 0; // read() contract mandates this
      if (bytesRead==limit)
         return -1;
      int toRead = Math.min(limit-bytesRead, len);
      int actuallyRead = super.read(data, offs, toRead);
      if (actuallyRead==-1)
          throw new UnexpectedEOFException();
      bytesRead += actuallyRead;
      return actuallyRead;
   }

   // similarly for the other read() methods

   // don't propagate to underlying stream
   public void close() { }
}
于 2010-06-08T16:23:53.843 回答
1

foos 是否在 FooRegistry 中注册?您可以尝试这种方法(假设 Bar 和 Baz 也有注册表来通过 id 获取引用)。

这可能有很多语法错误、使用错误等。但我觉得这种方法很好。

公共类 Foo {

public Foo(...) {
    //construct
    this.id = FooRegistry.register(this);
}

public Foo(long id, ...) {
    //construct
    this.id = id;
    FooRegistry.register(this,id);
}

}

public class FooRegistry() { Map foos = new HashMap...

long register(Foo foo) {
    while(foos.get(currentFooCount) == null) currentFooCount++;
    foos.add(currentFooCount,foo);
    return currentFooCount;
}

void register(Foo foo, long id) {
    if(foo.get(id) == null) throw new Exc ... // invalid
    foos.add(foo,id);
}

}

公共类酒吧(){

void writeToStream(OutputStream out) {
    out.print("<BAR><id>" + id + "</id><foo>" + foo.getId() + "</foo></BAR>");
}

}

公共类巴兹(){

void.writeToStream(OutputStream out) {
    out.print("<BAZ><id>" + id + "</id>");
    for(Bar bar : barList) out.println("<bar>" + bar.getId() + </bar>");
    out.print("</BAZ>");
}

}

于 2010-06-08T14:29:42.907 回答
1

我觉得我在重新发明轮子,必须有一种成熟的技术来处理所有的情况。

是的,看起来默认对象序列化会做,或者最终您正在预优化。

您可以更改序列化数据的格式(如XMLEncoder所做的那样)以获得更方便的格式。

但是,如果您坚持,我认为带有动态计数器的单例应该可以,但不要将 id 放在构造函数的公共接口中:

class Foo {
    private final int id;
    public Foo( int id, /*other*/ ) { // drop the int id
    }
 }

因此,该类可能是一个“序列”,并且可能更适合避免与Integer.MAX_VALUE.

使用java.util.concurrent.atomicAtomicLong包中描述的(以避免两个线程分配相同的 id,或避免过度同步)也会有所帮助。

class Sequencer {
    private static AtomicLong sequenceNumber = new AtomicLong(0);
    public static long next() { 
         return sequenceNumber.getAndIncrement();
    }
}

现在在你的每一堂课

 class Foo {
      private final long id;
      public Foo( String name, String data, etc ) {
          this.id = Sequencer.next();
      }
 }

就是这样。

(注意,我不记得反序列化对象是否调用构造函数,但你明白了)

于 2010-06-08T18:16:09.080 回答