2016-07-28 35 views
13

Ich habe die folgenden zwei Ebenen XML Struktur. Eine Liste von Boxen, die jeweils eine Liste von Schubladen enthalten.Iterieren über eine zweistufige Struktur mit verschachtelten Iteratoren

<Boxes> 
    <Box id="0"> 
     <Drawers> 
      <Drawer id="0"/> 
      <Drawer id="1"/> 
      ... 
     </Drawers> 
    </Box> 
    <Box id="1"> 
... 
    </Box> 
</Boxes> 

Ich bin Parsen es StAX mit und ausgesetzt, um die Struktur durch zwei Iterators:

  1. BoxIterator implements Iterator<Box>, Iterable<Box>
  2. Box implements Iterable<Drawer>
  3. DrawerIterator implements Iterator<Drawer>

ich dann folgendes tun:

BoxIterator boxList; 
for (Box box : boxList) { 
    for (Drawer drawer : box) { 
    drawer.getId() 
    } 
} 

Unter der Haube jener Iterators ich StAX bin mit und beide sind die gleichen zugrunde liegenden XMLStreamReader erreichbar. Wenn ich BoxIterator.next() rufe, beeinflusst das das Ergebnis, das bei nachfolgenden Aufrufen an DrawerIterator.next() zurückgegeben wird, weil der Cursor in das nächste Feld verschoben wurde.

Schaltet dies den Vertrag von Iterator? Gibt es eine bessere Möglichkeit, über eine zweistufige Struktur mit StAX zu iterieren?

+1

Ihre Beschreibung sieht aus wie 'Box.iterator' ein neues' DrawerIterator' zurückgibt und wenn das so der Vertrag gebrochen, da das wird nicht der Fall ist 'DrawerIterator' sollte sowieso nur Elemente innerhalb der aktuellen Box zurückgeben. – Thomas

+0

@Thomas 'Box.iterator()' gibt bei jedem Aufruf den gleichen 'DrawerIterator' zurück, da alle auf den gleichen zugrundeliegenden Stream zugreifen. Dies bedeutet, dass selbst ein 'DrawerIterator', der durch einen früheren Aufruf von' Box.iterator() 'zurückgegeben wird, magisch weiterentwickelt wird. Alle greifen immer auf den zugrunde liegenden Stream an derselben Cursorposition zu. – Roland

+0

Ah ich sehe. Das würde dann den Vertrag brechen. Müssen Sie bei jedem Anruf dieselbe Instanz zurückgeben? Wenn Sie jedes Mal eine neue Instanz zurückgeben und sequentiell iterieren würden (d. H. Kein zufälliger Zugriff), wäre es egal, ob die Cursorposition vorgerückt wäre. Nachdem Sie über eine Box iteriert haben, "fädeln Sie jeden weiteren Aufruf dieser Box" ein. DrawerIterator 'hasNext()' sollte false zurückgeben. – Thomas

Antwort

5

Schaltet dies den Vertrag von Iterator?

Nr

Die Java Iterator erlegt zwei "Verträge". Der erste Vertrag ist die Java-Schnittstelle selbst, die 3 Methoden deklariert: hasNext(), next() und remove(). Jede Klasse, die diese Schnittstelle Iterator implementiert, muss diese Methoden definieren.

Der zweite Auftrag definiert das Verhalten des Iterator:

hasNext() [...] liefert true, wenn die Iteration mehr Elemente hat. [...] next() liefert das nächste Element in der Iteration [und] löst NoSuchElementException aus, wenn die Iteration keine Elemente mehr enthält.

Das ist der gesamte Vertrag.

Es ist wahr, dass, wenn der zugrunde liegende XMLStreamReader fortgeschritten ist, kann es Ihre BoxIterator und/oder DrawerIterator durcheinander bringen. Alternativ könnte das Aufrufen der BoxIterator.next() und/oder DrawerIterator.next() an den falschen Stellen die Iteration durcheinander bringen. Jedoch korrekt verwendet, wie in Ihrem Beispielcode oben, funktioniert es richtig und vereinfacht den Code erheblich. Sie müssen nur die ordnungsgemäße Verwendung der Iteratoren dokumentieren.

Als ein konkretes Beispiel implementiert die Scanner Klasse Iterator<String> und hat noch viele, viele andere Methoden, die den zugrunde liegenden Stream voranbringen. Wenn es einen stärkeren Vertrag von der Klasse Iterator gegeben hätte, dann würde die Klasse Scanner selbst dies verletzen.


Als Ivan weist darauf hin, in den Kommentaren, sollte boxList nicht von class BoxIterator implements Iterator<Box>, Iterable<Box> Typ sein. Sie sollten wirklich haben:

class BoxList implements Iterable<Box> { ... } 
class BoxIterator implements Iterator<Box> { ... } 

BoxList boxList = ...; 
for (Box box : boxList) { 
    for (Drawer drawer : box) { 
    drawer.getId() 
    } 
} 

Während mit einer Klasse implementieren beide Iterable und Iterator ist technisch nicht falsch für Ihren Anwendungsfall, es zu Verwechslungen führen kann.

Betrachten Sie diesen Code in einem anderen Zusammenhang:

List<Box> boxList = Arrays.asList(box1, box2, box3, box4); 
for(Box box : boxList) { 
    // Do something 
} 
for(Box box : boxList) { 
    // Do some more stuff 
} 

Hier wird boxList.iterator() zweimal aufgerufen, zwei separate Iterator<Box> Instanzen zu erstellen, für zweimal die Liste der Boxen laufen. Da die boxList mehrfach durchlaufen werden kann, erfordert jede Iteration eine neue Iteratorinstanz.

In Ihrem Code:

BoxIterator boxList = new BoxIterator(xml_stream); 
for (Box box : boxList) { 
    for (Drawer drawer : box) { 
    drawer.getId(); 
    } 
} 

, weil Sie über einen Stream iterieren, können Sie nicht (ohne den Strom Zurückspulen oder die extrahierten Objekte zu speichern) ein zweites Mal über den gleichen Knoten durchlaufen. Eine zweite Klasse/Objekt wird nicht benötigt; Das gleiche Objekt kann sowohl als Iterable als auch als Iterator fungieren ... was Ihnen eine Klasse/Objekt erspart.

Having said that, ist vorzeitige Optimierung die Wurzel allen Übels. Die Ersparnisse einer Klasse/eines Objekts sind die mögliche Verwirrung nicht wert; Sie sollten BoxIterator in eine BoxList implements Iterable<Box> und BoxIterator implements Iterator<Box> teilen.

+1

Eigentlich ist das Codebeispiel nicht so gut, da die BoxIterator-Klasse sowohl Iterable als auch Iterator ist. Bei der zweiten Verwendung derselben Instanz kann es zu Problemen kommen, wenn der Status des Iterators nicht zurückgesetzt wird. –

+0

@IvanGammel Sie haben einen Punkt. BoxIterator gibt nur 'this' im Aufruf von' iterator() 'zurück und die Cursorposition auf dem zugrunde liegenden XMLStreamReader wird nicht zurückgesetzt. Also sollte ich vielleicht nicht das ganze Iterator-Iterable-Paradigma verwenden. Ich tat es nur, um die verbesserte for-Schleife, d. H. Syntaktischen Zucker, verwenden zu können. – Roland

+2

@Roland, obwohl es nicht die übliche Art ist, XML zu analysieren, ist Ihr Anwendungsfall gültig, wenn die Eingabe riesig ist und Sie ein kleines Heap-Limit haben (sonst könnten Sie die gesamte Datei mit XMLBeans oder XStream analysieren) verwende diesen Ansatz tatsächlich (sieht für mich wie ein Active Record-Muster aus). Sie müssen es nur vorsichtig implementieren. –

3

Es hat das Potenzial, den Vertrag für den Grund zu brechen, dass hasNext()true zurückkehren konnte, aber next() ein NoSuchElementException werfen konnte.

Der Vertrag von hasNext() ist:

Gibt true zurück, wenn die Iteration mehr Elemente hat. (Mit anderen Worten, gibt true zurück, wenn next() würde ein Element zurückgeben, anstatt eine Ausnahme zu werfen.)

Aber es könnte passieren, dass hasNext() und next() zwischen Aufruf, ein anderen Iterator die Stromposition so bewegt haben könnte, dass es sind keine Elemente mehr.

In der Art und Weise, wie Sie es verwendet haben (die verschachtelte Schleife), werden Sie jedoch nicht auf den Bruch stoßen.

Wenn Sie einen Iterator an einen anderen Prozess übergeben, kann dieser Bruch auftreten.

+0

Das Problem, auf das Sie hingewiesen haben, könnte mit jedem "Iterator" passieren, oder? Wenn Sie nach dem Aufruf von 'hasNext()' einen 'Iterator' an einen anderen Prozess übergeben, der ihn verbraucht, dann wird' next() 'Ihnen nicht das zurückgeben, was Sie erwartet haben. – Roland

+1

@Roland Ich meinte, dass die Angabe eines anderen * Iterators für einen anderen Prozess einen Iterator beeinflussen könnte. Der Aufruf von 'next()' betrifft * alle * Iteratoren, da sie die gleiche zugrunde liegende Eingabe haben. – Bohemian

+1

(Fast) Jeder Iterator teilt den zugrunde liegenden Zustand mit _etwas_. Und selbst wenn 'hasNext()' 'true' zurückgibt, garantiert es nicht, dass' next() ', falls _ unmittelbar danach aufgerufen wird, _always_ erfolgreich ist; Es könnte eine 'ConcurrentModificationException' auslösen. Iteratoren sind nur Helfer; Sie sind oft syntaktisch praktisch, aber sie garantieren niemals eine "nicht zerbrechliche oder beschädigte Iteration" über irgendeine Struktur. – AJNeufeld

0

Es sieht nicht wie es den Vertrag, den Sie sorgfältig vorgesehen brechen würde implementieren/zwingende next() & hasNext() Methoden in der BoxIterator & DrawerIterator von Iterator Schnittstelle implementieren. Unnötig zu sagen, ist die offensichtliche Bedingung zu beachten, dass hasNext()true zurückgeben sollte, wenn next() ein Element zurückgibt und false, wenn next() die Ausnahme gibt.

Aber was konnte ich nicht verstehe ist, warum haben Sie BoxIteratorIterable<Box> implementieren gemacht

BoxIterator implements Iterator<Box>, Iterable<Box> Da zwingende iterator() Methode von Iterable Schnittstelle für Box ist immer was los, eine Instanz von BoxIterator zurückzukehren. Wenn Sie kein anderes Ziel dahinter haben, gibt es keinen Zweck, dieses Feature in BoxIterator zu kapseln.

2

Das einzige Designproblem mit Ihrem Code ist, dass BoxIterator sowohl Iterator als auch Iterable implementiert. Normalerweise Iterable Objekt gibt neue stateful Iterator jedes Mal iterator() Methode aufgerufen wird. Aus diesem Grund sollte es keine Interferenz zwischen zwei Iteratoren geben, aber Sie benötigen ein Statusobjekt, um den Exit von der inneren Schleife korrekt zu implementieren (wahrscheinlich haben Sie das bereits, aber ich muss es aus Gründen der Klarheit erwähnen).

  1. Statusobjekt verhält sich wie ein Proxy für Parser mit zwei Methoden popEvent und peekEvent. Auf Peek-Iteratoren wird das letzte Ereignis überprüft, aber nicht konsumiert. Beim Pop werden sie das letzte Event konsumieren.
  2. BoxIterable#iterator() wird StartElement (Boxen) verbrauchen und Iterator danach zurückgeben.
  3. BoxIterator#hasNext() peek Ereignisse und Pop sie bis StartElement oder EndElement empfangen werden. Es wird dann nur dann true zurückgeben, wenn StartElement (Box) empfangen wurde.
  4. BoxIterator#next() Peek-and-Pop-Attribut-Ereignisse, bis StartElement oder EndElement empfangen, um Box-Objekt zu initialisieren.
  5. Box#iterator() wird StartElement (Schubladen) -Ereignis verbrauchen und dann DrawerIterator zurückgeben.
  6. DrawerIterator#hasNext() Peek-and-Pop bis StartElement oder EndElement erhalten. Es gibt dann nur dann true zurück, wenn StartElement (Drawer)
  7. DrawerIterator#next() Attributsereignisse verbraucht, bis EndElement (Drawer) empfangen wird.

wird Ihr Benutzercode fast unverändert bleiben:

BoxIterable boxList; 
/* 
* boxList must be an BoxIterable, which on call to iterator() returns 
* new BoxIterator initialized with current state of STaX parser 
*/ 
for (Box box : boxList) { 
    /* 
    * on following line new iterator is created and initialized 
    * with current state of parser 
    */ 
    for (Drawer drawer : box) { 
    drawer.getId() 
    } 
} 
+0

_Normalerweise gibt das Iterable-Objekt bei jedem Aufruf der Methode iterator() einen neuen Stateful-Iterator zurück. Dies ist hier nicht der Fall. 'BoxIterator' hat einen zugrundeliegenden' XMLStreamReader', daher gebe ich 'this' nur in der 'iterator()' Methode zurück. Der Status dieses Iterators ist die Position, an der sich der Cursor auf dem zugrunde liegenden Stream befindet. – Roland