В Java все типы перечислений и итераторов(такие как Iterator, ListIterator, SplitIterator) являются просто навигационными курсорами, и основная цель этих курсоров — перебирать элементы коллекции. Каждый курсор имеет свои особенности, преимущества и недостатки.
В этой статье мы рассмотрим эти итераторы, важные методы, преимущества и недостатки с примерами. Мы также поговорим о производительности различных курсоров в различных ситуациях, а также о лучших практиках.
1. Различные типы итераторов для коллекций
Помимо использования циклов for, типы коллекций позволяют реализовать итераторы для чистого кода, повышения производительности и манипулирования данными во время итераций.
В следующей таблице обобщены основные различия между устаревшим перечислением и различными итераторами.
Функции) | Перечисление | Итератор | ListIterator | Сплиттератор |
---|---|---|---|---|
Применимо к | Только устаревшие классы, такие как Vector, HashTable и Stack | Любой объект коллекции и потоки Java 8, поскольку это универсальный курсор | Только реализации List, такие как ArrayList, LinkedList, Stack и т. д. | Любые объекты коллекции, массивы и потоки |
Может выполнять | Только операции чтения | Операции чтения и удаления | Операции чтения, добавления, удаления и обновления | Только операции чтения, но также поддерживает параллельное чтение |
Может проходить в | Только прямое направление | Только прямое направление | Как в прямом, так и в обратном направлении | Только прямое направление |
Обработка | Серийный | Серийный | Серийный | Последовательный и параллельный |
Отказоустойчивый/быстрый | Отказоустойчивый | Fail-fast | Fail-fast | Fail-fast |
Использует | Внешняя итерация | Внешняя итерация | Внешняя итерация | Внутренняя итерация |
Безопасность потока | Потокобезопасный | Не потокобезопасно | Не потокобезопасно | Потенциально потокобезопасный |
Представлено в | Ява 1.0 | Ява 1.2 | Ява 1.2 | Ява 1.8 |
2. Перечисление Java для итерации по устаревшим классам
Интерфейс java.util.Enumeration — это устаревший интерфейс, доступный с JDK1.0, используемый для итерации по элементам устаревших классов коллекций.
В этом интерфейсе перечисления есть два важных метода.
//Tests if this enumeration contains more elements.boolean hasMoreElements();//Returns the next element of this enumeration if this enumeration object has at least one more element to provide.E nextElement();
2.1 Когда использовать перечисление
Мы можем использовать перечисление, когда нам нужно выполнить итерацию по элементам любых устаревших классов коллекций безотказным способом.
2.2 Пример перечисления
Мы можем получить объект Enumeration, вызвав метод .elements() для объекта класса коллекции.
Vector<Integer> vector = ...;Enumeration<Integer> enumeration = vector.elements();while(enumeration.hasMoreElements()){System.out.print(enumeration.nextElement() + " ");}
2.3 Преимущества и недостатки
Преимущества
- Это отказоустойчивый итератор, поэтому в случае итерации коллекции с использованием Enumeration мы никогда не получим никаких исключений, если во время итерации коллекция будет структурно изменена.
Недостатки
- Мы можем использовать Enumeration только для устаревших классов коллекций, поэтому он не является универсальным курсором.
- Используя перечисление, мы можем выполнять только операции чтения.
- Используя перечисление, мы можем двигаться только в прямом направлении.
- В случае очень больших коллекций процесс может быть медленным.
3. Итератор Java для простой итерации
Чтобы преодолеть некоторые из вышеперечисленных недостатков Enumeration, Java представила интерфейс java.util.Iterator в JDK1.2. Iterator может использоваться для итерации по элементам любых классов коллекций, представленных в Java, поэтому он является универсальным курсором. Итерируя с помощью Iterator, мы можем выполнять как операции чтения, так и операции удаления элементов коллекции.
Интерфейс Iterator имеет три важных метода.
//Returns true if the iteration has more elements.boolean hasNext();//Returns the next element in the iteration.E next();//Removes from the underlying collection the last element returned by this iterator.default void remove();
3.1 Когда использовать итератор?
Мы можем использовать итератор для перебора элементов любого класса коллекции с возможностью удаления любого элемента из коллекции.
3.2 Пример итератора
Следующая программа удаляет элемент 5 из изменяемого набора и выводит оставшиеся элементы.
Set<Integer> immutableSet = Set.of(1,2,3,4,5,6,7,8,9,10); // Immutable SetSet<Integer> mutableset = new HashSet<>(immutableSet); // Mutable SetIterator<Integer> iterator = mutableset.iterator();int num;while(iterator.hasNext()){if((num = iterator.next()) == 5) {iterator.remove();}else {System.out.print(num + " "); // # Prints '1 2 3 4 6 7 8 9 10'}}
3.3 Преимущества и недостатки
Преимущества
- Поскольку это универсальный курсор, мы можем использовать Iterator для любого класса коллекции.
- Итератор поддерживает операции чтения и удаления при переборе элементов любой коллекции.
Недостатки
- Используя итератор, мы можем выполнять только операции чтения и удаления, но не операции добавления и обновления.
- Используя итератор, мы можем двигаться только в прямом направлении.
- Это итератор с быстрым отказом.
- В случае очень больших коллекций процесс может быть медленным.
4. Java ListIterator для двунаправленной итерации по спискам
ListIterator можно использовать для итерации в прямом и обратном направлениях по элементам любых классов коллекций, реализованных на основе List. Используя ListIterator, мы можем выполнять операции чтения, удаления, добавления и обновления, одновременно итерируя по элементам коллекции.
Интерфейс ListIterator расширяет интерфейс Iterator, поэтому все методы, представленные в интерфейсе Iterator, по умолчанию доступны интерфейсу ListIterator.
Интерфейс ListIterator имеет девять важных методов.
// For iteration in forward directionboolean hasNext();E next(); //Returns the next element in the list and advances the cursor position.int nextIndex(); //Returns the index of the element that would be returned by a subsequent call to next();// For iteration in backward directionboolean hasPrevious();E previous(); //Returns the previous element in the list and moves the cursor position backwards.int previousIndex(); //Returns the index of the element that would be returned by a subsequent call to previous();// For operations on elementsvoid add(E e); //Inserts the specified element into the list.void set(E e); //Replaces the last element returned by next() or previous() with the specified element.void remove(); //Removes from the list the last element that was returned by next() or previous().
4.1. Когда использовать ListIterator?
Мы можем использовать ListIterator для итерации в прямом и обратном направлении по элементам классов коллекций, реализованных на основе List, с поддержкой добавления, обновления и удаления любого элемента из коллекции.
4.2 Пример ListIterator
Следующая программа демонстрирует различные операции над изменяемым списком. Программа использует цикл while для итерации по списку чисел с помощью итератора.
List<Integer> immutableList = List.of(1, 2, 3, 4, 5); // Immutable ListList<Integer> numbers = new ArrayList<>(immutableList); // Mutable ListListIterator<Integer> iterator = numbers.listIterator();int num;while(iterator.hasNext()) {num = iterator.next();if(num == 1) {iterator.remove(); //Removes '1'} else if(num == 5) {iterator.set(50); //Changes '5' to '50'}}iterator.add(6); //Adds '6'System.out.println(numbers); // [2, 3, 4, 50, 6]
4.3 Преимущества и недостатки
Преимущества
- ListIterator — самый мощный курсор, поскольку он поддерживает все операции чтения, добавления, удаления и обновления.
- Используя ListIterator, мы можем выполнять итерацию как в прямом, так и в обратном направлении.
Недостатки
- Мы можем использовать ListIterator только в классах коллекций, реализующих List.
- Это итератор с быстрым отказом.
- В случае очень больших коллекций процесс может быть медленным.
5. Java Spliterator для параллельной итерации больших коллекций
Во всех вышеперечисленных итераторах есть один общий недостаток: они медленные в случае очень больших коллекций. Чтобы решить эту проблему, Java представила интерфейс java.util.Spliterator в JDK1.8.
Интерфейс Spliterator — это внутренний итератор, который разбивает поток на более мелкие части. Эти более мелкие части могут обрабатываться параллельно. Очень редко мы используем Spliterator напрямую в нашей программе, мы используем его косвенно, вызывая методы stream() и parallelStream(), которые внутренне используют Spliterator.
См. также: Интерфейс Java Spliterator
Интерфейс Spliterator имеет четыре важных метода.
// Returns an estimate of the number of elements that would be encountered by a forEachRemaining traversal, or returns Long.MAX_VALUE if infinite, unknown, or too expensive to compute.long estimateSize();// If a remaining element exists, performs the given action on it, returning true; else returns false.boolean tryAdvance(Consumer<? super T> action);// Performs the given action for each remaining element, sequentially in the current thread, until all elements have been processed or the action throws an exception.default void forEachRemaining(Consumer<? super T> action);// if the spliterator can be partitioned, returns a Spliterator covering elements, that will, upon return from this method, not be covered by this Spliterator.Spliterator<T> trySplit();
5.1. Когда использовать Spliterator?
Мы можем использовать Spliterator для итерации и обработки элементов из очень большой коллекции, разделив коллекцию на несколько объектов Spliterator и обрабатывая их параллельно в разных потоках.
5.2 Пример сплиттератора
Следующая программа демонстрирует использование Spliterator и многопоточности для обработки большого списка строк. BigList создается с помощью функции Java 9+ под названием Stream.generate(). Затем он разбивает bigList на несколько частей для параллельной обработки с помощью метода trySplit() . Он создает новый Spliterator с именем split1.
Наконец, мы создаем два потока, которые обрабатывают обе половины списка и печатают каждый элемент. Обратите внимание, что оба потока работают одновременно, обрабатывая свои соответствующие половины списка параллельно.
List<String> bigList = Stream.generate(() -> "Hello").limit(30000).toList();Spliterator<String> split = bigList.spliterator();System.out.println(split.estimateSize()); // 30000Spliterator<String> split1 = split.trySplit();System.out.println(split.estimateSize()); // 15000System.out.println(split1.estimateSize()); // 15000new Thread(() -> split.forEachRemaining(elem -> System.out.println("TH1 " + elem))).start();new Thread(() -> split1.forEachRemaining(elem -> System.out.println("TH2 " + elem))).start();
5.3 Преимущества и недостатки
Преимущества
- Поскольку он поддерживает параллельную обработку, мы можем обрабатывать очень большие коллекции в разных потоках.
- Мы можем использовать Spliterator для любых объектов коллекции, массивов и потоков.
- Мы можем знать размер заранее.
Недостатки
- Используя Spliterator, мы можем выполнять только операции чтения и не можем изменять структуру коллекции.
- Используя Сплитератор, мы можем двигаться только вперед.
- Это итератор с быстрым отказом.
6. Производительность и передовой опыт
6.1 Варианты использования и производительность
- В случае устаревших классов коллекций наилучшую производительность обеспечивает Enumeration, поскольку это отказоустойчивый итератор.
- Iterator и ListIterator обеспечивают одинаковую производительность, но если нам нужно выполнять итерацию как в прямом, так и в обратном направлении, а также нужна поддержка дополнительных операций, таких как добавление и обновление, то ListIterator подойдет лучше всего.
- Если наш вариант использования заключается в параллельной обработке элементов очень большой коллекции в разных потоках, то Spliterator подходит лучше всего. Но имейте в виду, что при использовании Spliterator выполняйте только потокобезопасные операции, чтобы избежать состояний гонки.
6.2 Общие рекомендации
В общем случае, если размер коллекции не очень большой, то лучше всего использовать расширенный цикл for-each, который внутри себя использует Iterator.
for(T element : collection) {//...}
В случае, если коллекция большая по размеру, использование .stream() и .parallelStream() из Stream API является лучшей практикой, поскольку потоки работают лениво. Хотя Stream внутри использует только Spliterator.
big.stream().forEach(System.out::println);
7. Часто задаваемые вопросы
7.1 Различия между For-each и Iterator
Цикл for-each внутренне использует итератор для итерации по элементам коллекции. Следовательно, производительность цикла for-each и итератора одинакова.
- В случае итерации по элементам коллекции с использованием цикла for-each, если мы попытаемся изменить коллекцию структурно, мы получим ConcurrentModificationException. Но в случае итератора мы можем изменить коллекцию с помощью метода, предоставляемого итератором.
- В случае итерации по элементам коллекции с использованием итератора, если мы попытаемся получить следующий элемент(.next()) без проверки, существует ли следующий элемент(.hasNext()) или нет, мы получим NoSuchElementException, если следующего элемента нет. Но в случае цикла for-each такого случая нет.
- Мы можем использовать цикл for-each как для коллекции, так и для массива, а также можем использовать итератор для коллекций и потоков.
7.2 Различия между Iterator и ListIterator
Итератор | ListIterator |
Мы можем пересечь только в прямом направлении. | Мы можем перемещаться как в прямом, так и в обратном направлении. |
Это универсальный курсор, поэтому его можно применять ко всем классам коллекций. | Применимо только к классам коллекций, реализованным в List. |
Используя его, мы можем выполнять только операции чтения и удаления. | Используя его, мы можем выполнять операции чтения, добавления, удаления и обновления, поэтому это самый мощный курсор. |
Нет возможности получить какую-либо информацию, связанную с индексом элемента. | Мы можем получить индекс следующего элемента с помощью метода nextIndex(), а индекс предыдущего элемента — с помощью метода previousIndex(). |
7.3 Как найти индекс текущего элемента, на который указывает ListIterator?
Курсор ListIterator не указывает на элемент напрямую.
Согласно документации Java,
ListIterator не имеет текущего элемента; его позиция курсора всегда лежит между элементом, который будет возвращен вызовом previous(), и элементом, который будет возвращен вызовом next(). Итератор для списка длины n имеет n+1 возможных позиций курсора
Element(0) Element(1) Element(2) ... Element(n-1)cursor positions: ^ ^ ^ ^ ^
Следовательно, попеременные вызовы next() и previous() будут возвращать один и тот же элемент повторно.
List<Integer> numbers = new ArrayList<>(List.of(1,2,3,4,5));ListIterator<Integer> iterator = numbers.listIterator();System.out.println(iterator.next()); //1System.out.println(iterator.next()); //2System.out.println(iterator.previous()); //2System.out.println(iterator.next()); //2System.out.println(iterator.previous()); //2
7.4 Как оценить размер оставшихся элементов в Сплитераторе?
Чтобы узнать предполагаемый размер оставшихся элементов в Spliterator, интерфейс Spliterator определяет один метод с именем estimateSize(). Он возвращает оценку количества элементов, которые будут встречены при обходе forEachRemaining().
long estimateSize(); //returns the estimated size, or Long.MAX_VALUE if infinite, unknown, or too expensive to compute.
List<String> list = Stream.generate(() -> "Hello").limit(10).toList(); //10 elementsSpliterator<String> split = list.spliterator();split.tryAdvance(System.out::println); // Hellosplit.tryAdvance(System.out::println); // Hellosplit.tryAdvance(System.out::println); // HelloSystem.out.println(split.estimateSize()); // 7
7.5 Что такое поведение fail-fast и как оно связано с Iterator и ListIterator?
При итерации по элементам коллекции, если коллекция структурно изменена с использованием объекта коллекции, то при итерации по элементам курсор немедленно выдает исключение ConcurrentModificationException, такое поведение любого курсора известно как поведение fail-fast. За исключением Enumeration, все остальные курсоры(Iterator, ListIterator и Spliterator) по своей природе являются fail-fast.
Итераторы Fail-Fast внутри себя поддерживают флаг с именем modCount и вызывают метод checkForComodification(), чтобы проверить, была ли коллекция изменена или нет!
try {List<Integer> numbers = new ArrayList<>(List.of(1,2,3,4,5));ListIterator<Integer> iterator = numbers.listIterator();System.out.println(iterator.next()); // 1numbers.add(0, 10); //Modifies the collection. This should throw error !System.out.println(iterator.next());}catch(ConcurrentModificationException exception) {System.out.println("Attempted to the modify collection"); // Attempted to the modify collectionSystem.out.println(exception); // java.util.ConcurrentModificationException}
8. Заключение
В этом уроке мы изучили различия между Enumeration, Iterator, ListIterator и Spliterator с их важными методами и примером. Мы также изучили некоторые другие темы, такие как разница между циклом for-each и итератором, элемент, на который указывает listiterator, оценка размера оставшихся элементов в spliterator и поведение fail-fast итератора.