Short Assignment 6 is due Friday.
Lab 3 is due next Wednesday.
In recent lectures we saw linked lists in which the notion of the current element was part of the SentinelDLL
or SLL
object. Although in some ways this is convenient, in others it is not. For example, if some method is going through the list and passes the linked list to another method, that method can change the current element. It would be nice if there were some way that each method could have its own independent concept of the element in the list that it is currently dealing with.
In fact, we don't have to incorporate current
as an instance variable of SentinelDLL
or SLL
. We'll focus on modifying the SentinelDLL
class today, and we'll see how to make a separate object that knows how to traverse and modify a given list. By making it a separate object, we can have any number of them active at any time. In other words, we could have 0, 1, 2, 3, or any other number of such objects around, and each could have its own notion of the current element of the list. Our modification of the SentinelDLL
class will not have the instance variable current
, nor will it have anything like current
. Therefore it will not have get
, remove
, next
, hasNext
, previous
, hasPrevious
, add
, or set
methods.
This style of going through a data structure is so common that there's a name for it: an iterator. In fact, it's the basis of one of the standard interfaces in Java: the Iterator
interface.
Iterator
interfaceThe Iterator
interface consists of three methods:
hasNext
returns a boolean indicating whether there is a next element in the iteration through the data structure.next
returns a reference to the next object in the data structure, and it advances the iteration by one place.remove
deletes the object returned by the most recent call to next
. It might happen that there is no such object to delete, in which case remove
throws an IllegalStateException
. This method is optional, in that a class implementing the Iterator
interface must define it, but it's allowed to have an empty body. The remove
method should be called at most once each time next
is called.Iterators apply to lots of different data structures, not just linked lists. There is a general style of using the Iterator
interface. To demonstrate it, we need a class that allows us to get an Iterator
for the contents of the collection. The ArrayList
class is one such class. The driver in IteratedArrayListTest.java shows how.
If the Iterator
interface is implemented properly, then creating an object that implements the Iterator
interface starts an iteration. In IteratedArrayListTest.java, the iterator for an ArrayList
needs a reference to the ArrayList
, and it starts the iteration. Then we typically have a while-loop, whose header calls the hasNext
method. Within the body of the while-loop, a call to next
fetches the next element in the data structure, and the call to next
may be followed by a call to remove
. IteratedArrayListTest.java has two iterations through the ArrayList
: one to print out all elements and remove every other one, and one to show that the first iteration removed every other element.
We will sometimes see iterators used in for-loops rather than while-loops, but that's OK. After all, a for-loop is just a while-loop in disguise.
You might see similarities between the foreach-loops that we used to run through arrays and iterators. In fact, a foreach-loop for a collection of objects translates into code that uses iterators!. Foreach-loops work for arrays, ArrayList
s, and anything else that is "Iteratable." However, an iterator gives us one power that foreach-loops do not. It allows us to remove items.
ArrayList
How might we implement an iterator for an ArrayList
? The class IteratedArrayList.java does this. It extends
ArrayList
, and overrides the iterator
method to supply
its own iterator of type CS10Iterator
, an inner class. Note that this private inner class implements Iterator
and has a private constructor, but public methods. This means that any program that has a reference to a CS10Iterator
can call its methods, but the only way to create a
CS10Iterator
is to call the iterator
method of
IteratedArrayList
, which can see the private constructor of its inner
calls and calls it.
The class CS10Iterator
has three instance variables:
int position
that is the index of the position before the one that we will return when next
is called. It should be initialized to − 1.boolean nextWasCalled
that is true
if next
was called since the most recent remove
. It should be initialized to false
.myList
to the ArrayList
that created the iterator.The CS10Iterator
methods are implemented as follows:
hasNext
tests if position < list.size()-1
. (Note that when position == list.size()-1
you are saying that a call of next
should return list.get(list.size())
, which does not exist.)next
sets nextWasCalled
to true;
, increments position
, and returns list.get(position)
, but prints an
error message if there is no next item.remove
prints an error message if nextWasCalled
is false
. Otherwise it calls list.remove(position)
and sets nextWasCalled
to false
. The next item (and the rest of the ArrayList
) will be moved up one position, and so remove
also decrements position
.The main
program tests this code. Also, you can change the
initialization of myList
in IteratedArrayListTest.java to create a IteratedArrayList.java as another way to test the
iterator.
When we use an iterator in a linked list, we often want more functionality than the standard Iterator
interface provides. In fact, Java supplies a standard ListIterator
class. Its concept of "current" is different from the one we have seen. It has a "cursor position" between two elements in the list. A call to next
returns the item after the cursor and moves the cursor forward. A call to previous
returns the item before the cursor and moves the cursor backwards. Because of the way this works, alternating calls to next
and previous
will keep returning the same element. In addition to the methods in Iterator
, the ListIterator
interface requires the following methods:
previous
: return the previous element in the list and move the cursor back.hasPrevious
: return a boolean indicating whether there is a previous element.add
: add an element item at the current position, just before the cursor (so that a call to previous
would return that item and a call to next
would be unaffected).set
: replace the element item last returned by next
or previous
by obj
.remove
: in this interface, remove the element most recently last returned by a call to next
or previous
.nextIndex
: return the index of the element that would be returned by a call to next
.previousIndex
: return the index of the element that would be returned by a call to previous
.Calls to the remove
and set
methods are invalid if there has never been a call to next
or previous
or if remove
or add
has been called since the most recent call to next
or previous
.
The ArrayList
class has a method that returns a ListIterator
, also. There is a separate class LinkedList
, which behaves like our circular doubly-linked list with a sentinel. Both implement the interface List
, which requires a number of methods, including all that we saw for ArrayList
plus Iterator
, and ListIterator
. They differ in the amount of time operations take. For instance, a get
, set
, or add
on a LinkedList
requires time proportional to the distance that the index is from the nearest end of the list, but these operations take constant on an ArrayList
. On the other hand, an add
to either the front or end of a LinkedList
takes constant time, unlike an ArrayList
. If a ListIterator
is used, the time required for any method in the interface is constant. For an ArrayList
, the time for an add
or remove
is proportional to the number of items after the item added or removed, even if using a ListIterator
.
Because the conventions and operations are different from what we have implemented in SentinelDLL
we will show how to implement a ListIterator
using this new concept of the current element. We extend the Iterator
interface by declaring the CS10ListIterator
interface in CS10ListIterator.java. This interface does not have the
nextIndex
and previousIndex
that a normal Java ListIterator
requires.
Because we have removed some of the methods from the SentinelDLL
class, we need a new interface for the list class to implement. This new interface, CS10IteratedList
in CS10IteratedList.java, is similar to the LinkedList
interface in CS10LinkedList.java. The methods add
, remove
, get
, next
, and hasNext
—all of which require access to the current
instance variable—are gone.
There one new method: listIterator
. This method will return an object that can iterate through the object whose class implements CS10ListIterator
. This returned object starts an iteration.
SentinelDLLIterator
classSentinelDLLIterator.java is a modified version of the circular, doubly linked list with a sentinel that includes an iterator. The first thing to notice is that the SentinelDLLIterator
class implements the CS10IteratedList
interface, and so the methods that were in LinkedList
but not in CS10IteratedList
are missing from SentinelDLLIterator
.
The second thing to notice is that the SentinelDLLIterator
class has just sentinel
as an instance variable; there is no current
instance variable, as there was in SentinelDLL
.
But the most salient feature of our SentinelDLLIterator
class implementation is the inner class DLLIterator
, which implements the ListIterator
interface. The DLLIterator
class is private. Users of the SentinelDLLIterator
can still get a DLLIterator
by calling the listIterator
method. Moreover, because DLLIterator
implements the public CS10ListIterator
interface, once any part of any program has a reference to a DLLIterator
, it can call the public methods in CS10ListIterator
on it. The constructor is private, however, so that the only way to create a DLLIterator
object is to call the method listIterator
on a SentinelDLLIterator
object.
And, perhaps most importantly, by making DLLIterator
an inner class of SentinelDLLIterator
, the methods of DLLIterator
can access anything that the methods of SentinelDLLIterator
can access. That would include the instance variable sentinel
, as well as anything that is public
in the Element
class (such as data
, next
, and previous
).
The DLLIterator
class has two instance variables:
current
is chosen so that the implicit cursor is between current
and current.next
. This may seem a strange thing to do, but it allows us to go through a list, removing elements either forward or backward by alternately calling next
and remove
, or previous
and remove
.lastReturned
is a reference to the Element
whose data was returned by the most recent call to next
or previous
. This information is needed by remove
and set
. If next
or previous
was never called, or if a call to remove
or add
has changed the list since the last call to next
or previous
, this instance variable has the value null
.From how we've defined current
, it needs to be advanced in next
before we return an object when moving forward and after determining the object to return when moving backward. In order for everything to work, current
initially references the sentinel (rather than, say, sentinel.next
).
I have included an equals
method in DLLIterator
, and it is set so that two DLLIterator
objects are considered equal if they are currently referencing the same Element
. The code checks to ensure that both objects involved are DLLIterator
objects, and it returns false
if they're not.
Returning to the SentinelDLLIterator
class, there is a new method listIterator
. It creates a new DLLIterator
for the SentinelDLLIterator
object and returns a reference to it. This listIterator
method is made to be called from outside the SentinelDLLIterator
class, and because it returns a reference to a DLLIterator
, its return value may be assigned to CS10ListIterator
or even Iterator
(since ListIterator
extends Iterator
).
In the SentinelDLL
class, the toString
method now uses the iterator. Notice how toString
uses the iteration paradigm from before, with a while-loop whose test includes the call iter.hasNext
and whose body includes the call iter.next
.
The DLLIterator
created in toString
is independent of any other DLLIterator
in existence. Where one DLLIterator
's current
is has no effect at all on where another DLLIterator
's current
is.
We can really see this independence in ListTestIterator.java. Here, our test driver creates a DLLIterator
by the line
CS10ListIterator<String> iter = theList.listIterator();
The current
instance variable of this DLLIterator
is moved by next
and previous
and used by add
. But when we call theList.toString
, the DLLIterator
created and used by toString
does not affect the DLLIterator
in main
.
Similarly, the DLLIterator
created and used in calls to addFirst
and addLast
are independent of all others. Therefore adding to the front or back of a list does not change the current item in iter
.
I have also added a "clear" option that iterates through the list, removing all objects. (I could have used the clear
method, but chose not to). I have added a "print reversed" option that runs through the list backwards, after advancing to the end.
The "nested print" option really shows the power of separate iterators. Here, we have two DLLIterator
s, outer
and inner
. For each list object traversed by outer
, we perform a full traversal of the list with inner
. This task would be impossible if we were limited only to the methods we had in our original linked list implementations.
Having multiple iterators on the same object can be very useful, as we just saw. As long as none of them modifies the list everything is fine. Problems may arise, however, if any of the iterators modifies the list. In particular, if one iterator removes an element that is the current element of another iterator, things can get very messy. Even changing the list by using addFirst
and addLast
can change how things work, and calling clear
is definitely a problem!
Multiple threads (streams of control) can really cause problems. Suppose that you are on the second to last item in the list, you call hasNext
and true
is returned, and then call next
. Should be safe, right? Well, not if somebody else in another thread removed the last item between the two calls. (Maybe somebody clicked on a button or a Timer went off between the calls, and the method registered with the listener changed the list.)
Because of this potential, a bulletproof iterator should throw an exception if the list has been modified in any way except via the iterator's own operations. We won't worry about these situations for now.