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, ArrayLists, and anything else that is "Iteratable." However, an iterator gives us one power that foreach-loops do not. It allows us to remove items.
ArrayListHow 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 DLLIterators, 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.