Подтвердить что ты не робот

Итерация на двухуровневой структуре с использованием вложенных итераторов

У меня есть следующая структура уровня XML. Список ящиков, каждый из которых содержит список ящиков.

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

Я разбираю его с помощью StAX и выставляю структуру через два Iterators:

  • BoxIterator implements Iterator<Box>, Iterable<Box>
  • Box implements Iterable<Drawer>
  • DrawerIterator implements Iterator<Drawer>

Затем я могу сделать следующее:

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

Под капотом тех Iterators я использую StAX, и оба они получают доступ к одному и тому же базовому XMLStreamReader. Если я вызову BoxIterator.next(), это повлияет на результат, который будет возвращен при последующих вызовах на DrawerIterator.next(), потому что курсор переместится в следующее поле.

Разве это нарушает договор Iterator? Есть ли лучший способ перебора структуры двух уровней с помощью StAX?

4b9b3361

Ответ 1

Разве это нарушает договор Iterator?

Нет.

Java Iterator налагает два "контракта". Первый контракт - это сам интерфейс Java, который объявляет 3 метода: hasNext(), next() и remove(). Любой класс, который реализует этот интерфейс Iterator, должен определять эти методы.

Второй контракт определяет поведение Iterator:

hasNext() [...] возвращает true, если итерация имеет больше элементов. [...] next() возвращает следующий элемент в итерации [и] throws NoSuchElementException, если итерация не имеет больше элементов.

Это весь контракт.

Верно, что если базовый XMLStreamReader продвинут, он может испортить ваши BoxIterator и/или DrawerIterator. В противном случае вызов BoxIterator.next() и/или DrawerIterator.next() в неправильные моменты может испортить итерацию. Однако правильно используется, например, в приведенном выше примере кода, он работает правильно и значительно упрощает код. Вам просто нужно задокументировать правильное использование итераторов.

В качестве конкретного примера класс Scanner реализует Iterator<String>, и все же имеет много и много других методов, которые продвигают базовые поток. Если бы существовал более сильный контракт, наложенный классом Iterator, тогда сам класс Scanner нарушил бы его.


Как Ivan указывает в комментариях, boxList не должен иметь тип class BoxIterator implements Iterator<Box>, Iterable<Box>. Вы действительно должны иметь:

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

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

При использовании одного класса как Iterable, так и Iterator не является технически неправильным для вашего случая использования, это может вызвать путаницу.

Рассмотрим этот код в другом контексте:

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

Здесь boxList.iterator() вызывается дважды, чтобы создать два отдельных экземпляра Iterator<Box>, для повторного итерации списка ящиков дважды. Поскольку boxList может повторяться несколько раз, для каждой итерации требуется новый экземпляр итератора.

В вашем коде:

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

потому что вы выполняете итерацию по потоку, вы не можете (без перемотки потока или хранения извлеченных объектов) повторять по тем же узлам второй раз. Второй класс/объект не нужен; тот же объект может выступать как Iterable, так и Iterator..., который сохраняет вам один класс/объект.

Сказав это, преждевременная оптимизация - это корень всего зла. Экономия одного класса/объекта не стоит возможной путаницы; вы должны разделить BoxIterator на BoxList implements Iterable<Box> и BoxIterator implements Iterator<Box>.

Ответ 2

У этого есть потенциал, чтобы разорвать контракт по той причине, что hasNext() может возвращать true, но next() может выбросить NoSuchElementException.

Договор hasNext():

Возвращает true, если в итерации больше элементов. (Другими словами, возвращает true, если next() возвращает элемент, а не бросает исключение.)

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

Однако в том, как вы его использовали (вложенный цикл), вы не столкнетесь с поломкой.

Если вам нужно передать итератор другому процессу, вы можете столкнуться с этим поломкой.

Ответ 3

Единственная проблема с вашей версией кода заключается в том, что BoxIterator реализует как Iterator, так и Iterable. Обычно объект Iterable возвращает новый stateful Iterator каждый раз при вызове метода iterator(). Из-за этого не должно быть никаких помех между двумя итераторами, но вам понадобится объект состояния, чтобы правильно реализовать выход из внутреннего цикла (возможно, у вас уже есть это, но я должен упомянуть его для ясности).

  • Объект State будет действовать как прокси для парсера с двумя методами popEvent и peekEvent. На иетерах peek проверит последнее событие, но не будет его использовать. поп, они будут использовать последнее событие.
  • BoxIterable#iterator() будет потреблять StartElement (Boxes) и возвращать итератор после этого.
  • BoxIterator#hasNext() будет заглядывать в события и всплывать до тех пор, пока не будет получен StartElement или EndElement. Затем он вернет true, только если был получен StartElement (Box).
  • BoxIterator#next() будет вызывать события атрибутов Peek-and-pop до появления элемента StartElement или EndElement для инициализации объекта Box.
  • Box#iterator() будет потреблять событие StartElement (Ящики), а затем возвращает DrawerIterator.
  • DrawerIterator#hasNext() будет заглядывать и запускаться до получения значения StartElement или EndElement. Затем он вернет true, только если он был StartElement (Drawer)
  • DrawerIterator#next() будет принимать события Attribute до тех пор, пока не будет получен EndElement (Drawer).

Ваш код пользователя останется практически неизменным:

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

Ответ 4

Не похоже, что он нарушит контракт, если вы тщательно реализуете/переопределяете методы next() и hasNext() в BoxIterator и DrawerIterator, реализуя интерфейс Iterator. Само собой разумеется, очевидным условием заботы является то, что hasNext() должен возвращать true, если next() возвращает элемент и false, если next() предоставляет исключение.

Но я не мог понять, почему вы сделали BoxIterator реализацию Iterable<Box>

BoxIterator implements Iterator<Box>, Iterable<Box> Поскольку переопределение метода iterator() из интерфейса Iterable для Box всегда возвращает экземпляр BoxIterator. Если у вас нет других целей, то нет цели инкапсуляции этой функции в BoxIterator.