Java: назначение идентификаторов ссылки на объект для пользовательской сериализации - PullRequest
6 голосов
/ 08 июня 2010

По разным причинам у меня есть настраиваемая сериализация, где я выгружаю некоторые довольно простые объекты в файл данных.Может быть 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 предназначено только для сериализации, поэтому, когда я сериализуюсь в файл, я могу записывать объекты, сохраняя запись о том, какие идентификаторы были сериализованы до сих пор, а затем длякаждый объект проверяет, были ли его дочерние объекты сериализованы, и записывает те, которые этого не сделали, и, наконец, записывает сам объект, записывая его поля данных и идентификаторы, соответствующие его дочерним объектам.

Что меня удивляет, так это какназначить идентификаторы.Я думал об этом, и кажется, что есть три случая для назначения идентификатора:

  • динамически создаваемых объектов - идентификатор назначается из счетчика, который увеличивает
  • чтения объектов изdisk - идентификатор присваивается из номера, хранящегося в файле диска
  • singleton objects - объект создается до любого динамически создаваемого объекта, чтобы представлять одноэлементный объект, который всегда присутствует.

Как я могу справиться с этим правильно?Мне кажется, что я заново изобретаю колесо, и должна быть хорошо отработанная методика для обработки всех случаев.


уточнение: просто как некоторая тангенциальная информация, формат файлаЯ смотрю примерно следующее (зачеркнуть несколько деталей, которые не должны быть актуальными).Он оптимизирован для обработки довольно большого количества плотных двоичных данных (десятки / сотни МБ) с возможностью разброса структурированных данных в нем.Плотные двоичные данные составляют 99,9% от размера файла.

Файл состоит из серии блоков с исправленными ошибками, которые служат контейнерами.Каждый блок может рассматриваться как содержащий байтовый массив, который состоит из серии пакетов.Можно читать пакеты по одному за раз (например, можно сказать, где находится конец каждого пакета, а следующий начинается сразу после этого).

Таким образом, файл можно рассматривать каксерия пакетов, хранящихся поверх слоя с исправлением ошибок.Подавляющее большинство этих пакетов представляют собой непрозрачные двоичные данные, которые не имеют ничего общего с этим вопросом.Небольшое меньшинство этих пакетов, однако, составляют элементы, содержащие сериализованные структурированные данные, образующие своего рода «архипелаг», состоящий из «островков» данных, которые могут быть связаны ссылочными ссылками на объекты.

Так что у меня может быть файлгде пакет 2971 содержит сериализованный Foo, а пакет 12083 содержит сериализованный Bar, который ссылается на Foo в пакете 2971. (пакеты 0-2970 и 2972-12082 являются непрозрачными пакетами данных)

Все эти пакеты являются всеминеизменяемыми (и, следовательно, учитывая ограничения конструкции объектов Java, они образуют ациклический граф объектов), поэтому мне не приходится сталкиваться с проблемами изменчивости.Они также являются потомками общего Item интерфейса.Я хотел бы написать произвольный Item объект в файл.Если Item содержит ссылки на другие Item, которые уже есть в файле, мне нужно также записать их в файл, но только если они еще не были записаны.В противном случае у меня будут дубликаты, которые мне нужно будет как-то объединить, когда я прочитаю их обратно.

Ответы [ 3 ]

4 голосов
/ 08 июня 2010

Вам действительно нужно это сделать? Внутри ObjectOutputStream отслеживает, какие объекты уже были сериализованы. Последующие записи того же объекта сохраняют только внутреннюю ссылку (аналогично записи только идентификатора), а не записывают весь объект снова.

Подробнее см. Кэш сериализации .

Если идентификаторы соответствуют некоторому внешне определенному идентификатору, такому как идентификатор объекта, то это имеет смысл. Но вопрос гласит, что идентификаторы генерируются исключительно для отслеживания того, какие объекты сериализуются.

Вы можете обрабатывать синглтоны с помощью метода readResolve. Простой подход состоит в том, чтобы сравнить свеже десериализованный экземпляр с вашими одноэлементными экземплярами, и, если есть совпадение, вернуть одноэлементный экземпляр, а не десериализованный экземпляр. Э.Г.

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

РЕДАКТИРОВАТЬ: В ответ на комментарии поток в основном двоичные данные (хранятся в оптимизированном формате) со сложными объектами, распределенными в этих данных. Это может быть обработано с использованием формата потока, который поддерживает подпотоки, например почтовый индекс, или простой блок chunking. Например. поток может быть последовательностью блоков:

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() { }
}
1 голос
/ 08 июня 2010

Мне кажется, что я заново изобретаю колесо, и должна быть хорошо отработанная техника для обработки всех случаев.

Да, похоже на сериализацию объекта по умолчанию, или, в конечном счете, вы предварительно оптимизируете.

Вы можете изменить формат сериализованных данных (как XMLEncoder ) для более удобного.

Но если вы настаиваете, я думаю, что синглтон с динамическим счетчиком должен подойти, но не помещать идентификатор в открытый интерфейс конструктора:

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

Так что класс может быть «последовательностью» и, вероятно, более подходящим будет long, чтобы избежать проблем с Integer.MAX_VALUE.

Использование AtomicLong, как описано в пакете java.util.concurrent.atomic (во избежание назначения двум потокам одинакового идентификатора или во избежание чрезмерной синхронизации), также может помочь.

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

И это все.

(обратите внимание, я не помню, вызывает ли конструктор десериализация объекта, но вы поняли идею)

1 голос
/ 08 июня 2010

Зарегистрированы ли foos в FooRegistry? Вы можете попробовать этот подход (предположим, что Bar и Baz также имеют реестры для получения ссылок через идентификатор).

Это, вероятно, имеет много синтаксических ошибок, ошибок использования и т. Д. Но я считаю, что подход хорош

публичный класс Foo {

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

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

}

открытый класс FooRegistry () { Карта 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);
}

}

открытый класс Bar () {

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

}

открытый класс Baz () {

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

}

...