"Бесконечный" итератор плохой дизайн?
Считается ли это плохой практикой Iterator
реализации, которые являются "бесконечными"; т.е. куда звонит hasNext()
всегда (*) возвращать истину?
Как правило, я бы сказал "да", потому что вызывающий код может работать неправильно, но в приведенной ниже реализации hasNext()
вернет true, если вызывающая сторона не удалит все элементы из списка, с которым был инициализирован итератор; т.е. есть условие прекращения. Как вы думаете, это законное использование Iterator
? Это, похоже, не нарушает контракт, хотя я полагаю, что можно утверждать, что это не интуитивно понятно.
public class CyclicIterator<T> implements Iterator<T> {
private final List<T> l;
private Iterator<T> it;
public CyclicIterator<T>(List<T> l) {
this.l = l;
this.it = l.iterator();
}
public boolean hasNext() {
return !l.isEmpty();
}
public T next() {
T ret;
if (!hasNext()) {
throw new NoSuchElementException();
} else if (it.hasNext()) {
ret = it.next();
} else {
it = l.iterator();
ret = it.next();
}
return ret;
}
public void remove() {
it.remove();
}
}
(Педантичный) РЕДАКТИРОВАТЬ
Некоторые люди прокомментировали, как Iterator
может использоваться для генерации значений из неограниченной последовательности, такой как последовательность Фибоначчи. Тем не менее, Java Iterator
В документации говорится, что Итератор:
Итератор над коллекцией.
Теперь вы можете утверждать, что последовательность Фибоначчи является бесконечной коллекцией, но в Java я бы отождествил коллекцию с java.util.Collection
интерфейс, который предлагает такие методы, как size()
подразумевая, что коллекция должна быть ограниченной. Следовательно, законно ли использовать Iterator
как генератор значений из неограниченной последовательности?
11 ответов
Я думаю, что это полностью законно Iterator
это просто поток "вещей". Почему поток обязательно должен быть ограничен?
Многие другие языки (например, Scala) имеют концепцию неограниченных потоков, встроенных в них, и их можно повторять. Например, используя скалаз
scala> val fibs = (0, 1).iterate[Stream](t2 => t2._2 -> (t2._1 + t2._2)).map(_._1).iterator
fibs: Iterator[Int] = non-empty iterator
scala> fibs.take(10).mkString(", ") //first 10 fibonnacci numbers
res0: String = 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
РЕДАКТИРОВАТЬ: С точки зрения принципа наименьшего удивления, я думаю, что это полностью зависит от контекста. Например, что я ожидаю, что этот метод вернет?
public Iterator<Integer> fibonacciSequence();
Весь смысл Iterator
в том, что он ленивый, то есть он дает вам столько объектов, сколько вы просите. Если пользователь запрашивает все объекты бесконечного Iterator
Это их проблема, а не ваша.
Хотя я тоже думаю, что это законно, я хотел бы добавить, что такой Iterator
(или точнее: Iterable
производя такой Iterator
) не будет хорошо работать с расширенным циклом for (он же for-each-loop):
for (Object o : myIterable) {
...
}
Поскольку код внутри расширенного цикла for не имеет прямого доступа к итератору, он не может вызвать remove()
на итераторе.
Таким образом, чтобы завершить цикл, он должен выполнить одно из следующих действий:
- Получить доступ к внутренним
List
изIterator
и удалять объекты напрямую, возможно, провоцируяConcurrentModificationException
- использование
break
выйти из цикла - "использовать"
Exception
выйти из цикла - использование
return
выйти из цикла
Все, кроме последней из этих альтернатив, не совсем лучший способ оставить расширенный цикл for.
"Нормальный" цикл for (или любой другой цикл вместе с явным Iterator
переменная) будет работать нормально, конечно:
for (Iterator it = getIterator(); it.hasNext();) {
Object o = it.next()
...
}
Это может быть семантикой, но итератор должен старательно возвращать следующий элемент независимо от того, когда он закончится. Окончание является побочным эффектом для коллекции, хотя это может показаться общим побочным эффектом.
И все же, в чем разница между бесконечностью и коллекцией из 10 триллионов предметов? Звонящий либо хочет их всех, либо нет. Пусть звонящий решит, сколько предметов вернуть или когда закончить.
Я бы не сказал, что вызывающая сторона не может использовать конструкцию for-each. Он мог, пока он хочет все предметы.
Что-то в документации к коллекции, например "может быть бесконечной коллекцией", будет уместным, но не "бесконечный итератор".
Это совершенно законное использование - при условии, что оно надлежащим образом задокументировано.
Используя имя CyclicIterator
это хорошая идея, так как она подразумевает, что цикл на итераторе вполне может быть бесконечным, если случай выхода из цикла определен неправильно.
Бесконечный итератор очень полезен, когда вы создаете бесконечные данные, например, линейные рекуррентные последовательности, такие как последовательность Фибоначчи.
Так что это вполне нормально использовать такие.
Да. Вам просто нужны другие критерии, когда прекратить итерации.
Концепция бесконечных списков очень распространена в ленивых языках оценки, таких как Haskell, и приводит к совершенно разным проектам программ.
На самом деле, Iterator
исходит от Iterable
не Collection
что бы ни говорил API JavaDoc. Последнее расширяет первое, и поэтому оно может генерировать Iterator
но это вполне разумно для Iterable
не быть Collection
, На самом деле, есть даже несколько классов Java, которые реализуют Iterable
но нет Collection
,
Теперь, что касается ожиданий... конечно, бесконечный итератор не должен быть доступен без четкого указания, что это так. Но иногда ожидания могут быть обманчивы. Например, можно ожидать InputStream
всегда заканчиваться, но это действительно не является обязательным требованием. Аналогично Iterator
возврат строк, прочитанных из такого потока, может никогда не закончиться.
Подумайте, есть ли у вас функция генератора. Как вы заметили, итератор может быть разумным интерфейсом для генератора, а может и нет.
С другой стороны, в Python есть генераторы, которые завершают работу.
Я могу представить любое количество применений бесконечного итератора. Например, как насчет программы, перебирающей сообщения о состоянии, отправленные другим сервером или фоновым процессом. Хотя в реальной жизни сервер, по-видимому, рано или поздно останавливается, с точки зрения программы он может просто продолжать чтение, пока не будет остановлен чем-то не связанным с итератором, достигшим своего конца.
Это, безусловно, было бы необычно, и, как говорили другие, должно быть тщательно задокументировано. Но я бы не объявил это неприемлемым.
Если у вас есть объект, который нуждается в итераторе, и вам случается дать ему бесконечный итератор, то все должно быть хорошо. Конечно, этот объект никогда не должен требовать остановки итератора. И это потенциально плохой дизайн.
Подумайте о музыкальном проигрывателе. Вы говорите ему начать воспроизведение треков в "режиме непрерывного воспроизведения", и он будет воспроизводить треки, пока вы не скажете ему остановиться. Может быть, вы реализуете этот режим непрерывного воспроизведения, перетасовывая список воспроизведения навсегда... пока пользователь не нажмет "стоп". В этом случае, MusicPlayer
необходимо перебрать список воспроизведения, который может быть Collection<Track>
навсегда. В этом случае вы бы хотели CyclicIterator<Track>
как вы построили или ShuffleIterator<Track>
который случайным образом выбирает следующий трек и никогда не заканчивается. Все идет нормально.
MusicPlayer.play(Iterator<Track> playlist)
не волнует, заканчивается ли список воспроизведения или нет. Он работает как с итератором над списком треков (воспроизведение до конца, затем остановка), так и с ShuffleIterator<Track>
над списком треков (выберите случайный трек навсегда). Все по-прежнему хорошо.
Что если кто-то попытается написать MusicPlayer.playContinuously()
- который ожидает проигрывать треки "навсегда" (пока кто-то не нажмет "стоп"), но примет Iterator<Track>
как параметр? Это было бы проблемой дизайна. Очевидно, что playContinuously()
нужно бесконечное Iterator
и если он принимает равнину Iterator
как его параметр, то это будет плохой дизайн. Это, например, нужно проверить hasNext()
и обработать случай, когда это возвращает false
хотя это никогда не должно происходить. Плохой.
Поэтому реализуйте все бесконечные итераторы, которые вы хотите, до тех пор, пока их клиенты не зависят от окончания этих итераторов. Как будто ваш клиент зависит от того, что итератор работает вечно, тогда не давайте ему ничегоIterator
,
Это должно быть очевидно, но это не так.