19. Iterators

  • It is often necessary to access every element in some thing

    • Like an array, Stack, Queue, or Bag

  • This is called iterating over the things

  • This has already been done countless times with arrays

1for (int i = 0; i < someArray.length; i++) {
2    process(someArray[i]);
3}
  • However, not everything one may want to iterate over is an array

  • Trying to use a for loop for iterating over a linked structure doesn’t work as nicely

  • Instead, a while loop is typically used

1while (currentNode != null) {
2    process(currentNode.getData());
3    currentNode = currentNode.getNext();
4}

19.1. Iterators

  • Java provides a common uniform way to iterate over things

  • Iterators are objects that allow iterating over something one element at a time

    • Get each element in an array

    • Get each element from a Bag

  • There are two important relevant interfaces:

    • Iterator — used when creating an iterator object

    • Iterable — used when creating something one may want to iterate over

19.2. Iterator Interface

  • Iterator objects are typically very simple

  • To define an iterator for a class, define the class such that it implements Iterator<T>

  • There are two abstract methods included in the interface to focus on

    • T next() — retrieve the next element

    • boolean hasNext() — checks if another element exists

  • With these, it becomes possible to iterate over elements without needing to worry about the underlying container

1Iterator<T> iterator = arbitraryIterableThing.iterator();
2
3while (iterator.hasNext()) {
4    process(iterator.next());
5}

19.2.1. Array Iterator

  • Although it is not required to know what the underlying container is when using the iterator

  • It is required to define an iterator for the underlying container

  • For example, if defining a collection that makes use of an array, an ArrayIterator will need to be defined

5public class ArrayIterator<T> implements Iterator<T> {
6
7    private final int size;
8    private final T[] elements;
9    private int index;
  • The fields only include

    • size — number of elements in the collection

    • elements — the array of elements to iterate over

    • index — the current index the iterator is at

13    public ArrayIterator(T[] elements, int size) {
14        if (size > elements.length) {
15            throw new IllegalArgumentException("size exceeds array length.");
16        }
17        this.elements = elements;
18        this.size = size;
19        this.index = 0;
20    }
  • Constructor sets the iterator up to start at the beginning of the array

24    @Override
25    public boolean hasNext() {
26        return index < size;
27    }
  • hasNext returns a boolean indicating if there is an element to retrieve

  • Simply check if the current index (index) is less than the number of elements in the array (size)

31    @Override
32    public T next() {
33        if (!hasNext()) {
34            throw new NoSuchElementException();
35        }
36        T returnElement = elements[index];
37        index++;
38        return returnElement;
39    }
  • next returns the next element and updates index

    • It returns the element and sets the iterator up to return the subsequent element when needed

  • The way this is written, if next is called when there are no more elements to retrieve, an exception is thrown

  • Notice that

    • This iterator can only go in one direction

    • Once the iterator object hits the end of the collection, it does not reset

    • To start iterating again, a new iterator would need to be created

Note

There is nothing preventing someone from writing an iterator class that returns the element in some other order. For example, reverse order.

Warning

Consider that the ArrayIterator has reference to the underlying array container. Generally, iterators should not modify the collections they are iterating over. Side effects like this are a recipe for disaster.

19.2.2. Linked Iterator

 1import java.util.Iterator;
 2import java.util.NoSuchElementException;
 3
 4public class LinkedIterator<T> implements Iterator<T> {
 5
 6    private Node<T> current;
 7
 8    public LinkedIterator(Node<T> head) {
 9        current = head;
10    }
11
12    @Override
13    public boolean hasNext() {
14        return current != null;
15    }
16
17    @Override
18    public T next() {
19        if (!hasNext()) {
20            throw new NoSuchElementException();
21        }
22        T returnElement = current.getData();
23        current = current.getNext();
24        return returnElement;
25    }
26}
  • For the LinkedIterator, all that is needed is a reference to the current node

  • The constructor sets the current node to reference the head of the linked structure

  • hasNext just checks if current references null

  • next returns the next element and sets the iterator up to return the subsequent element when needed

  • Just like the ArrayIterator

    • The iterator only goes in one direction

    • Once an element is retrieved with next(), it is not possible to retrieve it again unless creating a new iterator

Warning

If the Node class is an internal class, then the LinkedIterator will need to be internal too.

19.2.3. Collection Iterators

  • Consider a SortedBag

  • If using an ArraySortedBag implementation, the iterator method would need to return an ArrayIterator

    1    @Override
    2    public Iterator<T> iterator() {
    3        return new ArrayIterator<>(bag, size());
    4    }
    
  • Similarly, the iterator method for a LinkedSortedBag would need to return a LinkedIterator

    1    @Override
    2    public Iterator<T> iterator() {
    3        return new LinkedIterator<>(head);
    4    }
    
  • Since both versions of the SortedBag return an Iterator

  • And since the iterator provides a common way to iterate over the container

  • It is possible to iterate over an array and linked structure the exact same way

1    Iterator<Integer> iterator = myBag.iterator();
2
3    while (iterator.hasNext()) {
4        process(iterator.next());
5    }
  • In the end, the actual underlying container has no impact on the code used to iterate

19.2.3.1. toString

  • Consider the following toString for some collection

1    public String toString() {
2        StringBuilder builder = new StringBuilder();
3        Iterator<T> iterator = this.iterator();
4        while(iterator.hasNext()) {
5            builder.append(iterator.next());
6            builder.append(", ");
7        }
8        return builder.toString();
9    }
  • By looking at the above code, it is not possible to know if this is for an array or linked implementation

    • This is a fantastic example of abstraction

    • It is possible to iterate over something (what) without needing to worry about the implementation details (how)

19.3. Iterable Interface

  • The Iterator interface is used for creating an iterator object to iterate over something

  • If making a class that is to be iterated over, then that class will implement Iterable<T>

    • For example, asking the collection for an iterator

      • myBag.iterator()

  • Within the Iterable interface there is one abstract method — iterator()

  • If this interface is implemented correctly, the collection is guaranteed to be iterable

19.3.1. For Each

  • For things that are iterable, enhanced for loop can be used — for each loop

  • In general, it looks like this

1for (type refVar: iterableThing) {
2    process(refVar);
3}
  • An example for our SortedBag<Integer> may look like this

1for (Integer x: myBag) {
2    process(x);
3}
  • Revisiting the toString() example from above with an enhanced for loop

1    public String toString() {
2        StringBuilder builder = new StringBuilder();
3        for (T element : this) {     // 'this' is the iterable thing
4            builder.append(element);
5            builder.append(", ");
6        }
7        return builder.toString();
8    }

Note

The enhanced for loop is just syntactic sugar for what was already shown above. The enhanced for loop example

1for (Integer x: myBag) {
2    process(x);
3}

means the same thing as

1Iterator<Integer> iterator = myBag.iterator();
2while (iterator.hasNext()) {
3    process(iterator.next());
4}

19.4. Equality of Stacks (and Queues)

114    @Override
115    public final boolean equals(Object o) {
116        if (this == o) {
117            return true;
118        }
119        if (o == null || getClass() != o.getClass()) {
120            return false;
121        }
122        ArrayStack<?> that = (ArrayStack<?>) o;
123        return Arrays.equals(this.stack, 0, this.size(), that.stack, 0, that.size());
124    }
125
126    @Override
127    public final int hashCode() {
128        int result = Objects.hash(top);
129        for (int i = 0; i < size(); i++) {
130            result = result * 97 + Objects.hashCode(stack[i]);
131        }
132        return result;
133    }
 85    @Override
 86    public boolean equals(Object o) {
 87        if (this == o) {
 88            return true;
 89        }
 90        if (o == null || getClass() != o.getClass()) {
 91            return false;
 92        }
 93        LinkedStack<?> that = (LinkedStack<?>) o;
 94        if (this.size() != that.size()) {
 95            return false;
 96        }
 97        Node<?> thisCurrent = this.top;
 98        Node<?> thatCurrent = that.top;
 99        while (thisCurrent != null && thatCurrent != null) {
100            if (!Objects.equals(thisCurrent.getData(), thatCurrent.getData())) {
101                return false;
102            }
103            thisCurrent = thisCurrent.getNext();
104            thatCurrent = thatCurrent.getNext();
105        }
106        return true;
107    }
108
109    @Override
110    public int hashCode() {
111        int result = Objects.hashCode(size);
112        Node<?> current = top;
113        while (current != null) {
114            result = result * 97 + Objects.hashCode(current.getData());
115            current = current.getNext();
116        }
117        return result;
118    }
  • It should be possible to check equality between Stack objects, regardless of their implementation

  • However, with the current implementation, it’s not possible to compare an ArrayStack and LinkedStack

  • A solution to this problem is to make Stacks implement iterable

  • If this is done, there is a common way to iterate over the Stack independent of the underlying implementation

 1@Override
 2public final boolean equals(Object o) {
 3    if (this == o) {
 4        return true;
 5    }
 6    if (o == null) {
 7        return false;
 8    }
 9    if (!(o instanceof Stack)) {
10        return false;
11    }
12    Stack<?> that = (Stack<?>) o;
13    if (this.size() != that.size()) {
14        return false;
15    }
16    Iterator<T> thisIterator = this.iterator();
17    Iterator<?> thatIterator = that.iterator();
18    while (thisIterator.hasNext()) {
19        if (!Objects.equals(thisIterator.next(), thatIterator.next())) {
20            return false;
21        }
22    }
23    return true;
24}

19.5. For Next Time

  • Consider how the Stack and Queue could be made iterable

  • Read Chapter 7

    • 12 pages

19.5.1. Playing Code