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

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

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

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


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

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

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

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

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


person Jason S    schedule 08.06.2010    source источник
comment
Нужно ли вам беспокоиться о случае, когда вы создали некоторые объекты, а затем будете загружать некоторые из них с диска (с идентификаторами, которые могут конфликтовать с уже существующими)?   -  person VeeArr    schedule 08.06.2010
comment
Да, и я думаю, что подход заключается в том, чтобы рассматривать идентификаторы как два отдельных пространства имен и сопоставлять уже существующие идентификаторы с новыми.   -  person Jason S    schedule 08.06.2010
comment
объекты должны идти в отдельных пакетах? Можно ли их всех поместить в один большой пакет? Ссылаются ли на объектные пакеты непрозрачные двоичные данные? Я подозреваю, что логические отношения смешиваются с физическим хранилищем.   -  person mdma    schedule 08.06.2010
comment
Ссылаются ли на объектные пакеты из непрозрачных двоичных данных: косвенно да (например, они интерпретируются с использованием объектных пакетов). Это, а также тот факт, что я довольно быстро передаю эти пакеты в файл и хочу, чтобы они записывались как можно быстрее, делает очень важным записывать их в файл вскоре после их создания. Я мог бы играть в игры, удерживая их в памяти и имея дело с процессом очистки, который обрабатывает случай, когда генератор пакетов прерывается на полпути во время записи пакетов на диск. вот тут посложнее....   -  person Jason S    schedule 09.06.2010
comment
(и я отношусь к этому как к потоку, а не как к файлу с произвольным доступом, а именно, я не хочу возвращаться и перезаписывать части файла позже — это отделило бы порядок пакетов от порядка, в котором они записываются в файл, но в стоимость сложности)   -  person Jason S    schedule 09.06.2010
comment
Позвольте мне еще раз проверить: двоичные данные связаны с объектами. Итак, у вас может быть пакет объекта, а затем последующие пакеты двоичных данных интерпретируются с использованием объекта как контекста?   -  person mdma    schedule 09.06.2010
comment
да... ну, пакеты бинарных данных интерпретируются с использованием объектов, появившихся на данный момент в качестве контекста. (объекты можно рассматривать как события, которые представляют собой небольшие неизменяемые объекты, представляющие изменения в изменяемом контексте)   -  person Jason S    schedule 09.06.2010


Ответы (3)


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

Дополнительные сведения см. в разделе кэш сериализации.

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

Вы можете обрабатывать синглтоны с помощью метода 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() { }
}
person mdma    schedule 08.06.2010
comment
+1 за то, что указали.... Мне действительно нужно это делать? Я бы с удовольствием использовал какое-нибудь средство, встроенное в JRE, но между ObjectOutputStream и тем, чем я занимаюсь, так много различий, что я не знаю, как связать их вместе. Моя сериализация ближе к сериализации XML. - person Jason S; 08.06.2010
comment
Пробовали ли вы XStream — xstream.codehaus.org. Это сериализация, но на основе XML. Очень подключаемый. Он также использует кэш сериализации — ссылки на уже сериализованные объекты записываются как ссылки в XML, либо ссылаясь на автоматически сгенерированный идентификатор, либо используя XPath для ссылки на исходный элемент, который определил объект. Стоит посмотреть. - person mdma; 08.06.2010
comment
Я действительно посмотрел за несколько минут до публикации комментария. Моя проблема в этом конкретном случае заключается в том, что мне нужно вставить несколько сложных объектов среди большого набора байтов необработанных данных в двоичном коде, которые необходимо хранить оптимизированным способом, поскольку они используют 99,9% пространства файла, и я ожидание файлов в диапазоне 10-100 МБ. Так что я не могу использовать XML... все, что у меня есть, это куча разрозненных островков среди большого потока данных. - person Jason S; 08.06.2010
comment
XStream позволяет вам полностью заменить фактический формат файла, поэтому вы можете использовать FastInfoset или какой-либо другой двоичный стандарт. Я предполагаю, что ваш формат файла позволяет вам получить острова данных и рассматривать их как подпотоки основного потока. Затем вы можете хранить там все, что хотите, XML, FastInfoSet, буферы протоколов и т. д. Просто потому, что остальная часть вашего файла оптимизирована в двоичном виде, не означает, что все это должно быть. Вы можете использовать фрагментацию, чтобы отделить островки данных от остальной части потока. Я подробнее остановлюсь в своем ответе. - person mdma; 08.06.2010
comment
глупый вопрос... как реализовать подпоток? - person Jason S; 08.06.2010
comment
(например, каждый блок знает свою длину, я так и делаю) - person Jason S; 08.06.2010
comment
Не глупый вопрос - я обновил свой ответ. - person mdma; 08.06.2010
comment
Хорошо, я вижу, к чему ты клонишь. Возможно, «острова» — плохой термин; на самом деле у меня есть архипелаг данных. Я обновлю свой вопрос, чтобы уточнить. - person Jason S; 08.06.2010
comment
Принято... В итоге я сохранил данные в виде островов и использовал Google gson для кодирования каждого из них в нотации JSON. У меня есть возможность дублировать некоторые объекты в файле данных, но они составляют настолько небольшую часть размера файла, что это не имеет значения для размера файла, и если меня волнует эквивалентность графа объектов, я могу объединить несколько объектов. копии эквивалентных объектов после их считывания из файла. - person Jason S; 09.06.2010
comment
Это звучит хорошо. Я собирался предложить расширить ObjectOutputStream, чтобы он записывал пакеты данных, принадлежащие объекту, после потоковой передачи объекта. Это позволит сохранить граф объектов без дубликатов, позволяя каждому объекту записывать принадлежащие ему данные. - person mdma; 09.06.2010

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

}

person corsiKa    schedule 08.06.2010

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

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

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

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

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

Таким образом, класс может быть «последовательностью», и, вероятно, более подходящим будет длинный, чтобы избежать проблем с 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();
      }
 }

Вот и все.

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

person OscarRyz    schedule 08.06.2010
comment
??? это сбивает с толку... у вас есть Sequencer как класс с нестатическими методами, но вы вызываете Sequencer.next(), как если бы next был статическим методом. Кроме того, я ценю помощь, но я знаю, как сделать то, что вы говорите, для создания экземпляра счетчика; мой вопрос больше связан с тем, как управлять либо присваиванием на основе счетчика, или обратным чтением из файла или статическим синглтоном. Я не могу использовать только один подход для конструкторов - person Jason S; 08.06.2010