7 Stimmen

java: Zuweisung von Objektreferenz-IDs für benutzerdefinierte Serialisierung

Aus verschiedenen Gründen habe ich eine benutzerdefinierte Serialisierung, wo ich einige ziemlich einfache Objekte in eine Datendatei dumpen bin. Es gibt vielleicht 5-10 Klassen, und die Objektgraphen, die sich daraus ergeben, sind azyklisch und ziemlich einfach (jedes serialisierte Objekt hat 1 oder 2 Verweise auf ein anderes, das serialisiert wird). Zum Beispiel:

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 */) { ... }
}

Das id-Feld ist nur für die Serialisierung, so dass ich beim Serialisieren in eine Datei Objekte schreiben kann, indem ich aufzeichne, welche IDs bisher serialisiert wurden, dann für jedes Objekt überprüfe, ob seine untergeordneten Objekte serialisiert wurden, und diejenigen schreibe, die nicht serialisiert wurden, und schließlich das Objekt selbst schreibe, indem ich seine Datenfelder und die IDs, die seinen untergeordneten Objekten entsprechen, schreibe.

Was mir Rätsel aufgibt, ist die Zuweisung von IDs. Ich habe darüber nachgedacht, und es scheint, dass es drei Fälle für die Zuweisung einer ID gibt:

  • dynamisch erstellte Objekte -- id wird von einem Zähler zugewiesen, der inkrementiert
  • Lesen von Objekten von der Festplatte -- id wird anhand der in der Datei gespeicherten Nummer zugewiesen
  • Singleton-Objekte - Das Objekt wird vor jedem dynamisch erstellten Objekt erstellt, um ein Singleton-Objekt darzustellen, das immer vorhanden ist.

Wie kann ich diese richtig behandeln? Ich habe das Gefühl, dass ich das Rad neu erfinden muss, und es muss doch eine bewährte Technik für alle Fälle geben.


Klärung: Nur zur Information: Das Dateiformat, das ich im Auge habe, ist ungefähr das folgende (wobei ich ein paar Details auslasse, die nicht relevant sein sollten). Es ist optimiert, um eine ziemlich große Menge an dichten Binärdaten (zehn/hunderte von MB) zu verarbeiten, mit der Möglichkeit, strukturierte Daten darin einzuschleusen. Die dichten Binärdaten machen 99,9 % der Dateigröße aus.

Die Datei besteht aus einer Reihe von fehlerbereinigten Blöcken, die als Container dienen. Jeder Block kann als ein Byte-Array betrachtet werden, das aus einer Reihe von Paketen besteht. Es ist möglich, die Pakete nacheinander zu lesen (z. B. kann man feststellen, wo das Ende eines jeden Pakets ist, und das nächste beginnt unmittelbar danach).

Man kann sich die Datei also als eine Reihe von Paketen vorstellen, die über einer Fehlerkorrekturschicht gespeichert werden. Die überwiegende Mehrheit dieser Pakete sind undurchsichtige Binärdaten, die nichts mit dieser Frage zu tun haben. Eine kleine Minderheit dieser Pakete sind jedoch Elemente, die serialisierte strukturierte Daten enthalten und eine Art "Archipel" aus Daten-"Inseln" bilden, die durch Objektreferenzbeziehungen verbunden sein können.

Ich könnte also eine Datei haben, in der Paket 2971 ein serialisiertes Foo enthält, und Paket 12083 enthält ein serialisiertes Bar, das sich auf das Foo in Paket 2971 bezieht. (wobei die Pakete 0-2970 und 2972-12082 undurchsichtige Datenpakete sind)

Alle diese Pakete sind unveränderlich (und bilden daher aufgrund der Beschränkungen der Java-Objektkonstruktion einen azyklischen Objektgraphen), so dass ich mich nicht mit Problemen der Veränderbarkeit befassen muss. Sie sind außerdem Nachkommen eines gemeinsamen Item Schnittstelle. Was ich tun möchte, ist, eine beliebige Item Objekt in der Datei. Wenn die Item enthält Verweise auf andere Item s, die sich bereits in der Datei befinden, muss ich diese auch in die Datei schreiben, aber nur, wenn sie noch nicht geschrieben wurden. Andernfalls habe ich Duplikate, die ich irgendwie zusammenführen muss, wenn ich sie zurücklese.

0 Stimmen

Müssen Sie sich Gedanken über den Fall machen, dass Sie einige Objekte erstellt haben und dann einige von der Festplatte laden (mit IDs, die möglicherweise mit bereits vorhandenen Objekten in Konflikt geraten könnten)?

0 Stimmen

Ja, und ich denke, der Ansatz besteht darin, die IDs als zwei getrennte "Namensräume" zu behandeln und die bereits vorhandenen IDs den neuen zuzuordnen.

0 Stimmen

Müssen die Objekte in getrennten Paketen geliefert werden? Können sie alle in ein großes Paket gepackt werden? Werden die Objektpakete von den undurchsichtigen Binärdaten referenziert? Ich habe den Verdacht, dass die logischen Beziehungen mit dem physischen Speicher verwechselt werden.

4voto

mdma Punkte 55529

Müssen Sie das wirklich tun? Intern ist die ObjectOutputStream verfolgt, welche Objekte bereits serialisiert worden sind. Nachfolgende Schreibvorgänge desselben Objekts speichern nur eine interne Referenz (ähnlich wie beim Schreiben nur der ID), anstatt das gesamte Objekt erneut zu schreiben.

参照 Serialisierungs-Cache für weitere Einzelheiten.

Wenn die IDs einer extern definierten Identität entsprechen, wie z. B. einer Entitäts-ID, dann ist dies sinnvoll. In der Frage heißt es jedoch, dass die IDs nur erzeugt werden, um zu verfolgen, welche Objekte serialisiert werden.

Sie können Singletons über die Funktion readResolve Methode. Ein einfacher Ansatz besteht darin, die frisch deserialisierte Instanz mit Ihren Singleton-Instanzen zu vergleichen und bei einer Übereinstimmung die Singleton-Instanz anstelle der deserialisierten Instanz zurückzugeben. Z.B..

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

EDIT: Um auf die Kommentare zu antworten: Der Stream besteht größtenteils aus Binärdaten (die in einem optimierten Format gespeichert sind) mit komplexen Objekten, die in diesen Daten verstreut sind. Dies kann durch die Verwendung eines Stream-Formats, das Substreams unterstützt, z. B. zip, oder durch einfaches Block Chunking gehandhabt werden. Der Stream kann z. B. eine Folge von Blöcken sein:

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

Sie können dann Blöcke für Binärdaten, Blöcke für serialisierte Daten, Blöcke für XStream serialisierte Daten usw. haben. Da jeder Block seine Größe kennt, können Sie einen Substream erstellen, der bis zu dieser Länge von der Stelle in der Datei liest. Auf diese Weise können Sie Daten beliebig mischen, ohne sich um das Parsen zu kümmern.

Um einen Stream zu implementieren, lassen Sie Ihren Hauptstream die Blöcke parsen, z. B.

   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
      ...

Eine Skizze von LimitInputStream sieht so aus:

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

0 Stimmen

+1 für die Erstellung des Punktes.... Muss ich das wirklich tun? Ich würde gerne eine in die JRE integrierte Funktion verwenden, aber es gibt so viele Unterschiede zwischen ObjectOutputStream und dem, was ich tue, dass ich nicht weiß, wie ich die beiden miteinander verknüpfen kann. Meine Serialisierung ist näher an der XML-Serialisierung.

0 Stimmen

Haben Sie XStream ausprobiert? xstream.codehaus.org . Es ist eine Serialisierung, die aber auf XML basiert. Sehr anpassungsfähig. Es verwendet auch einen Serialisierungs-Cache - Verweise auf bereits serialisierte Objekte werden als Referenzen in XML geschrieben, entweder mit Bezug auf eine automatisch generierte ID oder mit XPath, um auf das ursprüngliche Element zu verweisen, das das Objekt definiert. Ein Blick lohnt sich.

0 Stimmen

Ich habe tatsächlich ein paar Minuten nachgeschaut, bevor ich einen Kommentar abgegeben habe. Mein Problem in diesem speziellen Fall ist, dass ich ein paar komplexe Objekte unter einem großen Satz von binär codierten Rohdaten Bytes, die in einer optimierten Weise gespeichert werden müssen, da sie 99,9% des Speicherplatzes der Datei verwenden und ich erwarte Dateien im Bereich 10-100MB einstreuen muss. Ich kann also kein XML verwenden... alles, was ich habe, ist ein Haufen unzusammenhängender Inseln in einem größeren Datenstrom.

1voto

corsiKa Punkte 79125

Sind die Foos bei einer FooRegistry registriert? Sie könnten diesen Ansatz ausprobieren (gehen Sie davon aus, dass Bar und Baz auch Registrierungen haben, um die Referenzen über die id zu erhalten).

Dies enthält wahrscheinlich viele Syntaxfehler, Anwendungsfehler usw. Aber ich denke, der Ansatz ist ein guter.

public class 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);
}

}

public class Bar() {

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

}

public class Baz() {

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

}

1voto

OscarRyz Punkte 189898

Ich habe das Gefühl, dass ich das Rad neu erfinden muss, und es muss doch eine bewährte Technik für alle Fälle geben.

Ja, sieht aus wie Standard-Objekt-Serialisierung tun würde, oder letztlich Sie Pre-Optimierung sind.

Sie können das Format der serialisierten Daten ändern ( wie die XMLEncoder ) für eine bequemere Variante.

Aber wenn Sie darauf bestehen, denke ich, dass das Singleton mit dynamischen Zähler tun sollte, aber setzen Sie nicht die id, in der öffentlichen Schnittstelle für den Konstruktor:

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

Die Klasse könnte also eine "Sequenz" sein, und wahrscheinlich wäre ein "long" angemessener, um Probleme mit dem Integer.MAX_VALUE .

Die Verwendung eines AtomicLong wie beschrieben in der java.util.concurrent.atomic (um zu vermeiden, dass zwei Threads dieselbe ID zuweisen, oder um eine übermäßige Synchronisierung zu vermeiden) wäre ebenfalls hilfreich.

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

Jetzt haben Sie in jeder Klasse

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

Und das war's.

(Anmerkung: Ich weiß nicht mehr, ob die Deserialisierung des Objekts den Konstruktor aufruft, aber Sie verstehen schon)

0 Stimmen

Das ist verwirrend... Sie haben Sequencer als eine Klasse mit nicht statischen Methoden, aber Sie rufen Sequencer.next() auf, als ob next eine statische Methode ist. Außerdem weiß ich die Hilfe zu schätzen, aber ich weiß, wie man das macht, was Sie sagen, um einen Zähler zu instanziieren; meine Frage geht eher in die Richtung, wie man entweder eine zählerbasierte Zuordnung oder Rücklesen aus der Datei oder ein statisches Singleton. Ich kann nicht nur einen Ansatz für Konstruktoren verwenden

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X