Итерация на двухуровневой структуре с использованием вложенных итераторов
У меня есть следующая структура уровня 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
?
Ответы
Ответ 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
.