Сегодня поговорим о том, что такое Iterator в Java и зачем он нужен.
Итератор (Iterator) в Java — это объект, который позволяет последовательно перебирать элементы коллекции, не раскрывая её внутреннюю структуру. Используя методы hasNext() для проверки наличия следующего элемента и next() для его получения, итератор обеспечивает унифицированный способ обхода List, Set и Map.
Как ты уже, вероятно, знаешь, в Java есть замечательный интерфейс Collection, реализующий интерфейс Iterator. Сразу оговорюсь, не следует путать интерфейс iterator с паттерном iterator в Java!![Паттерн Iterator - 1]()
Представим на секунду, что iterator в Java отсутствует. В таком случае всем и каждому придётся нырнуть в самые глубины коллекций и по-настоящему разобраться, чем отличается,
Знакомство с интерфейсом Iterator
И дабы внести ясность, для начала разберёмся именно с интерфейсом.
| Дословно «Iterator» можно перевести как «переборщик». То есть это некая сущность, способная перебрать все элементы в коллекции. При этом она позволяет это сделать без вникания во внутреннюю структуру и устройство коллекций. |
ArrayList от LinkedList и HashSet от TreeSet.
Методы, которые должен имплементировать Iterator
boolean hasNext() — если в итерируемом объекте (пока что это Collection) остались еще значение — метод вернет true, если значения кончились false.
E next() — возвращает следующий элемент коллекции (объекта). Если элементов больше нет (не было проверки hasNext(), а мы вызвали next(), достигнув конца коллекции), метод бросит NoSuchElementException.
void remove() — удалит элемент, который был в последний раз получен методом next(). Метод может бросить:
UnsupportedOperationException, если данный итератор не поддерживает методremove()(в случае с read-only коллекциями, например)IllegalStateException, если методnext()еще не был вызван, или еслиremove()уже был вызван после последнего вызоваnext().
Использование итераторов с коллекциями
Итак, iterator для List — самая распространенная имплементация. Итератор идет от начала коллекции к ее концу: смотрит есть ли в наличии следующий элемент и возвращает его, если таковой находится. На основе этого несложного алгоритма построен циклfor-each.
Его расширение — ListIterator. Рассмотрим дополнительные методы java list iterator. Ты их, скорее всего, знаешь:
void add(E e)— вставляет элементEвList;boolean hasPrevious()— вернетtrue, если при обратном перебореListимеются элементы;int nextIndex()— вернет индекс следующего элемента;E previous()— вернет предыдущий элемент листа;int previousIndex()— вернет индекс предыдущего элемента;void set(E e)— заменит элемент, возвращенный последним вызовомnext()илиprevious()на элементe.
List, содержащий в себе строки приветствия обучающихся:
List<String> list = new ArrayList<>();
list.add("Привет");
list.add("Обучающимся");
list.add("На");
list.add("JavaRush");
Теперь получим для него итератор и выведем в консоль все содержащиеся строки:
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
Сейчас будет «узкое место»: Java Collections как ты, вероятно, знаешь (а если не знаешь, разберись), расширяют интерфейс Iterable, но это не означает, что только List, Set и Queue поддерживают итератор. Для java Map iterator также поддерживается, но его необходимо вызывать для Map.entrySet():
Map map = new HashMap<>();
Iterator mapIterator = map.entrySet().iterator();
Iterator> mapIterator = map.entrySet().iterator();
Тогда метод next() будет возвращать объект Entry, содержащий в себе пару «ключ»-«значение».
Дальше все аналогично с List:
while (mapIterator.hasNext()) {
Map.Entry entry = mapIterator.next();
System.out.println("Key: " + entry.getKey());
System.out.println("Value: " + entry.getValue());
}
Интерфейс vs. Паттерн: В чем разница?
Ты думаешь: «Стоп. Мы говорим про интерфейс, а в заголовке статьи написано «Паттерн». То есть, паттерн iterator – это интерфейс Iterator? Или интерфейс — это паттерн?» Если это слово встречается впервые, даю справку: паттерн — это шаблон проектирования, некое поведение, которого должен придерживаться класс или множество взаимосвязанных классов. Итератор в java может быть реализован для любого объекта, внутренняя структура которого подразумевает перебор, при этом можно изменить сигнатуру обсуждаемых методов. Главное при реализации паттерна – логика, которой должен придерживаться класс. Интерфейс итератор – частная реализация одноименного паттерна, применяемая как к готовым структурам (List, Set, Queue, Map), так и к прочим, на усмотрение программиста. Расширяя интерфейс Iterator, ты реализуешь паттерн, но для реализации паттерна не обязательно расширять интерфейс. Простая аналогия: все рыбы плавают, но не всё, что плавает – рыбы.Создание собственного итератора
В качестве примера я решил взять… слово. А конкретнее — существительное. Оно состоит из частей: приставки, корня, суффикса и окончания. Для частей слова создадим интерфейсWordPart и классы, расширяющие его: Prefix, Root, Suffix и Ending:
interface WordPart {
String getWordPart();
}
static class Root implements WordPart {
private String part;
public Root(String part) {
this.part = part;
}
@Override
public String getWordPart() {
return part;
}
}
static class Prefix implements WordPart {
private String part;
public Prefix(String part) {
this.part = part;
}
@Override
public String getWordPart() {
return part;
}
}
static class Suffix implements WordPart {
private String part;
public Suffix(String part) {
this.part = part;
}
@Override
public String getWordPart() {
return part;
}
}
static class Ending implements WordPart {
private String part;
public Ending(String part) {
this.part = part;
}
@Override
public String getWordPart() {
return part;
}
}
Тогда класс Word (слово) будет содержать в себе части, а кроме них добавим целое число, отражающее количество частей в слове:
public class Word {
private Root root;
private Prefix prefix;
private Suffix suffix;
private Ending ending;
private int partCount;
public Word(Root root, Prefix prefix, Suffix suffix, Ending ending) {
this.root = root;
this.prefix = prefix;
this.suffix = suffix;
this.ending = ending;
this.partCount = 4;
}
public Word(Root root, Prefix prefix, Suffix suffix) {
this.root = root;
this.prefix = prefix;
this.suffix = suffix;
this.partCount = 3;
}
public Word(Root root, Prefix prefix) {
this.root = root;
this.prefix = prefix;
this.partCount = 2;
}
public Word(Root root) {
this.root = root;
this.partCount = 1;
}
public Root getRoot() {
return root;
}
public Prefix getPrefix() {
return prefix;
}
public Suffix getSuffix() {
return suffix;
}
public Ending getEnding() {
return ending;
}
public int getPartCount() {
return partCount;
}
public boolean hasRoot() {
return this.root != null;
}
public boolean hasPrefix() {
return this.prefix != null;
}
public boolean hasSuffix() {
return this.suffix != null;
}
public boolean hasEnding() {
return this.ending != null;
}
Окей, у нас есть четыре перегруженных конструктора (для простоты, предположим, что суффикс у нас может быть только один). Существительное не может состоять из одной приставки, поэтому для конструктора с одним параметром будем устанавливать корень.
Теперь напишем реализацию паттерна итератор: WordIterator, переопределяющий 2 метода: hasNext() и next():
import java.util.Iterator;
import java.util.NoSuchElementException;
public class WordIterator implements Iterator {
private final Word word;
private int currentPartIndex;
public WordIterator(Word word) {
this.word = word;
this.currentPartIndex = 0;
}
@Override
public boolean hasNext() {
return currentPartIndex < word.getPartCount();
}
@Override
public Word.WordPart next() {
if (!hasNext()) {
throw new NoSuchElementException("Больше нет частей в этом слове!");
}
// Задаем обратный порядок, как в оригинальной статье: окончание -> суффикс -> приставка -> корень
int partToReturnIndex = word.getPartCount() - 1 - currentPartIndex;
currentPartIndex++;
switch (partToReturnIndex) {
case 3:
return word.getEnding();
case 2:
return word.getSuffix();
case 1:
return word.getPrefix();
case 0:
return word.getRoot();
default:
// Эта логика зависит от того, как устроен класс Word и его конструкторы
// Для простоты будем отталкиваться от корня при меньшем кол-ве частей
if (word.hasRoot()) return word.getRoot();
if (word.hasPrefix()) return word.getPrefix();
if (word.hasSuffix()) return word.getSuffix();
return word.getEnding();
}
}
@Override
public void remove() {
throw new UnsupportedOperationException("Удаление частей слова не поддерживается!");
}
}
Осталось назначить итератор классу Word:
public class Word implements Iterable {
//…
@Override
public Iterator iterator() {
return new WordIterator(this);
}
//…
}
Теперь проведем морфемный разбор слова «перебежка»:
public class Main {
public static void main(String[] args) {
Word.Root root = new Word.Root("беж");
Word.Prefix prefix = new Word.Prefix("пере");
Word.Suffix suffix = new Word.Suffix("к");
Word.Ending ending = new Word.Ending("a");
Word word = new Word(root, prefix, suffix, ending);
Iterator wordIterator = word.iterator();
while (wordIterator.hasNext()) {
Word.WordPart part = wordIterator.next();
System.out.println(part.getClass().getSimpleName() + ": " + part.getWordPart());
}
}
}
Обрати внимание, в своей реализации паттерна iterator я выбрал следующий порядок вывода:
- окончание
- суффикс
- приставка
- корень
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ