"Бесконечный" итератор плохой дизайн?

Считается ли это плохой практикой 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,

Это должно быть очевидно, но это не так.

Другие вопросы по тегам