Oct 9: Iterators, with List examples
Code discussed in lecture
- Swarm.java
- Bug.java
- CS10ListIterator.java
- CS10IteratedList.java
- CS10LinkedList.java
- SentinelDLLIterator.java
- ListTestIterator.java
Iterators
In recent lectures we saw linked lists in which the notion of the current element was part of theSentinelDLL
or
SLL
object. While 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
,
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.
The Iterator
interface
The 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 tonext
. It might happen that there is no such object to delete, in which caseremove
throws anIllegalStateException
.
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.
We can do the following:
hasNext
, that the
next call to next
will succeed.
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, arraylists, and anything else that is "Iteratable". However, an iterator gives us one power that the foreach for loops did not. It allows us to remove items.
To see this in an application, I have re-implemented
Swarm.java from the CS 5 Bugs
homework to use an ArrayList
and iterators. The swarm
keeps track of bugs on the screen, and removes them if they crawl off
of the screen or are clicked on for elimination. The
Bug.java is included for completeness. (If you
have not taken CS 5 don't worry; I am just going to show three
short methods that show how interators are used. I thought that it would
be better to take them from a real program than to just make up something.) Note that to
use the Iterator
interface, I needed to include the
line
The relevant methods for our purposes are draw
,
move
, and hitbugs
. The draw
method
simply goes through the ArrayList insects
in order,
with no deletions. Therefore we can simply use a foreach loop:
The code for move
also illustrates the use of
remove
. The move
method returns true
if the bug is still visible and false
if it has crawled off of the canvas.
Because we want to remove, we have to use an iterator.
insects
. Note that I include Bug
in
the iterator's type, to say that it's next
will always return a Bug.
I have a while
loop to continue as long as there is a next. Within the loop I get the next
Bug
and move it. If move
returns
false I use iter.remove()
to remove it from the ArrayList
.
The call to remove
removes the object most recently returned by
next
. Note that such a remove does not change the element returned
by the next call to next
.
The code for hitBugs
again illustrates the use of
remove
. This time we avoid using a temporary variable by calling
isHit
directly on the Bug
returned by next
.
Implementing an iterator for an arraylist
How might you implement such an iterator for an arraylist? This is one way to do it.Create an object that has three instance variables:
- A
int position
that is the index of the position before the one that you will return if you callnext()
. Note that this is also the position of the thing that you last callednext()
on, if you callednext()
. It you should be initialized to -1. - A
boolean nextWasCalled
that is true ifnext()
was called since the most recentremove()
. It should be initialized tofalse
. - A reference
list
to the arraylist that created the interator.
hasNext
tests ifposition < list.size()-1
. (Note that whenposition == list.size()-1
you are saying that thenext()
should returnlist.get(list.size())
, which does not exist.)next
setsnextWasCalled
totrue;
, incrementsposition
, and returnslist.get(position)
.remove
throws an exception ifnextWasCalled
is false. Otherwise it callslist.remove(position)
and sets setsnextWasCalled
tofalse
. The next item (and the rest of the arraylist) will be moved left one position, soposition
must be decremented.
An iterator interface for a linked list
When we use an iterator in a linked list, we often want more functionality than the standardIterator
interface provides. In fact,
Java supplies a standard ListIterator
class. Its concept
of "current" is different than the one we have seen. It has a "cursor position" between
two elements in the list. (Draw picture.) 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
it requires methods:
previous()
- returns previous element in list, moves current.hasPrevious()
- returns true if there is a previous item.add(Object obj)
- add item at current position, just before the cursor (so that a call toprevious
would return that item and a call tonext
would be unaffected).set(Object obj)
- replace item last returned bynext
orprevious
byobj
.remove()
- in this interface removes item last returned bynext
orprevious
.nextIndex()
- returns the index of the element that would be returned by a call tonext
.previousIndex()
- returns the index of the element that would be returned by a call toprevious
.
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. 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. That means
that 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 than what we have
implemented in SentinelDLL
we will show how to implement
a ListIterator
using this new concept of current.
We extend the Iterator
interface by declaring the CS10ListIterator
interface in CS10ListIterator.java.
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
.
The SentinelDLLIterator
class
SentinelDLLIterator.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. However, the constructor
is private, 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 (like
data
, next
, and previous
).
The DLLIterator
class has an two instance variables:
-
current
is chosen so that the implicit cursor is betweencurrent
andcurrent.next
. This may seem a strange thing to do, but it allows one to go through a list deleting things either forward or backwards by alternately callingnext
andremove
orprevious
andremove
. -
lastReturned
is a reference to theElement
whose data was returned by the last call tonext
orprevious
. This information is needed byremove
andset
. Ifnext
orprevious
was never called, or if a call toremove
oradd
has changed the list since the last call tonext
orprevious
, this instance variable has the valuenull
.
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 backwards.
That also
means that initially, current
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
.
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
).
Using the iterator
In theSentinelDLL
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
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
s created and used in the
call theList.addFirst(name)
is independent of all others.
Therefore adding to the front of a list does not change the current item
in iter
. (Why did I not use an iterator in
addLast
?)
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.
A Problem with Multiple Iterators
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. However, list modifications can be a problem. 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 usingaddFirst
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, an interator should throw an exception if the list has been modified in any way except via the iterator's own operations.