Java: назначение идентификаторов ссылки на объект для пользовательской сериализации

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

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

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

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


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

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

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

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

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

3 ответа

Решение

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

Смотрите Serialization Cache для более подробной информации.

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

Вы можете обращаться с одиночками через 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() { }
}

Зарегистрированы ли 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 () {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);
}

}

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

}

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

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

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

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

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

И это все.

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

Другие вопросы по тегам