Lists


Contents


The List ADT

Our first ADT is the list: an ordered collection of items. Recall that the external view includes the "conceptual picture" and the set of "conceptual operations". The conceptual picture of a list is something like this:

      item 0
      item 1
      item 2
      . . .
      item n
and one reasonable set of operations is:

OPERATION DESCRIPTION
list() (constructor) create an empty list
void add(Object ob) add ob to the end of the list
void add(int pos, Object ob) add ob at position pos in the list, moving the items originally in positions pos through size() one place to the right to make room (error if pos is less than 0 or greater than size())
boolean contains(Object ob) return true iff ob is in the list (i.e., there is an item x in the list such that x.equals(ob))
int size() return the number of items in the list
boolean isEmpty() return true iff the list is empty
Object get(int pos) return the item at position pos in the list (error if pos is less than 0 or greater than or equal to size())
Object remove(int pos) remove and return the item at position pos in the list, moving the items originally in positions pos+1 through size() one place to the left to fill in the gap (error if pos is less than 0 or greater than or equal to size())

Many other operations are possible; when designing an ADT, you should try to provide enough operations to make the ADT useful in many contexts, but not so many that it gets confusing. It is not always easy to achieve this goal; it will sometimes be necessary to add operations in order for a new application to use an existing ADT.

Now let's consider the "private" or "internal" part of the list ADT. In other words, how should we actually implement lists in Java? First, note that Java classes are a good match with the idea of ADTs: the headers of the public methods correspond to the public (external) part of an ADT, and the method bodies, as well as the private fields and methods correspond to the private (internal) part of the ADT. So let's use a Java class for our list ADT.

Note that we could define different classes for different kinds of lists (e.g., an IntList contains only integers, a StringList contains only strings, etc). Another alternative (which we will choose here) is to define the List class as a list of Objects; that way, we only need one class definition, and we can use different instances of the class to store different kinds of items (we can even have a list that contains more than one kind of item, e.g., both Strings and Integers).

We will consider two ways to implement the List class: using an array, and using a linked list (the former is covered in this set of notes; the latter in the next set of notes).


TEST YOURSELF #1

Question 1.
What other operations on Lists might be useful? Define them by writing descriptions like those in the table above.

Question 2.
Note that the second add method (the one that adds an item at a given position) can be called with a position that is equal to size, but for the get and remove methods, the position has to be less than size. Why?

Question 3.
Another useful abstract data type is called a Map. A Map stores unique "key" values with associated information. For example, you can think of a dictionary as a map, where the keys are the words, and the associated information is the definitions.

What are some other examples of Maps that you use?

What are the useful operations on Maps? Define them by writing descriptions like those in the table above.


Array Implementation

Here's an outline of the Java definition for the List class, implemented using an array to store the items in the list (the bodies of the methods are not filled in for now):

Note that the public methods provide the "external" view of the list ADT. Looking only at the "signatures" of the public methods (the method names, return types, and parameters), plus the descriptions of what they do (e.g., as provided in the table above), a programmer should be able to write code that uses Lists. It is not necessary for a client of the List class to see how the List methods are actually implemented. The private fields and the bodies of the methods provide the "internal" view -- the actual implementation.

Implementing the add methods

Now let's think about how to implement some of the List methods. First we'll consider the add method that has just one parameter (the object to be added to the list). That method adds the object to the end of the list. Here is a conceptual picture of what add does:
      BEFORE:  item 0    item 1    item 2    ...    item n
 

      AFTER:   item 0    item 1    item 2    ...    item n   NEW ITEM

Now let's think about the actual code. First, note that the array that stores the items in the list (the items array) may be full. If it is, we'll need to:

  1. get a new, larger array, and
  2. copy the items from the old array to the new array.
Then we can add the new item to the end of the list.

Note that we'll also need to deal with a full array when we implement the other add method (the one that adds a new item in a given position). Therefore, it makes sense to implement handling that case (the two steps listed above) in a separate method, expandArray that can be called from both add methods. Since handling a full array is part of the implementation of the List class (it is not an operation that users of the class need to know about) the expandArray method should be a private method.

Here's the code for the first add method, and for the expandArray method.

In general, when you write code it is a good idea to think about special cases. For example, does add work when the list is empty? When there is just one item? When there is more than one item? You should think through these cases (perhaps drawing pictures to illustrate what the list looks like before the call to add, and how the call to add affects the list). Decide if the code works as is, or if some modifications are needed.

Now let's think about implementing the second version of the add method (the one that adds a item at a specified position in the list). An important difference between this version and the one we already implemented is that for this version a bad value for the pos parameter is considered an error. In general, if a method detects an error that it doesn't know how to handle, it should throw an exception. (Note that exceptions should not be used for other purposes like exiting a loop.) More information about exceptions is provided in a separate set of notes.

So the first thing our add method should do is check whether parameter pos is in range, and if not, throw an IndexOutOfBoundsException. If pos is OK, we must check whether the items array is full, and if so, we must call expandArray. Then we must move the items in positions pos through numItems - 1 over one place to the right to make room for the new item. Finally, we can insert the new item at position pos, and increment numItems. Here is the code:


TEST YOURSELF #2

Question 1.
Write the remove and get methods.


Implementing the constructor

Now let's think about the constructor function, which should initialize the fields so that the list is empty. Clearly, numItems should be set to zero. How about the items field? It could be set to null, but that would mean another special case in the add methods. A better idea would be to initialize items to refer to an array with some initial size, perhaps specified using a static final field, so that the initial size could be easily changed.

Below is the code for the constructor (including the declaration of the static field for the initial size). This code uses 10 as the initial size. In practice, the appropriate initial size will probably depend on the context in which the List class is used. The advantage of a larger initial size is that more "add" operations can be performed before it is necessary to expand the array (which requires copying all items). The disadvantage is that if the initial array is never filled, then memory is wasted. The requirements for memory usage and runtime performance of the application that uses the List class, as well as the expected sizes of the lists that it uses should be used to determine the appropriate initial size.

Iterators

What are they?

When a client uses an abstract data type that represents a collection of items (as the List class does), they often need a way to iterate through the collection, i.e., to access each of the items in turn. Our get method can be used to support this operation. Given a List L, we can iterate through the items in the list as follows:

However, a more standard way to iterate through a collection of items is to use an Iterator, which is an interface defined in java.util. Every Java class that implements the Collection interface provides an iterator method that returns an Iterator for that collection.

The way to think about an Iterator is that it is a finger that points to each item in the collection in turn. When an Iterator is first created, it is pointing to the first item; a next method is provided that lets you get the item pointed to, also advancing the pointer to point to the next item, and a hasNext method lets you ask whether you've run out of items. For example, assume that we have added an iterator method (that returns an Iterator) to our List class, and that we have the following list of words:

If we create an Iterator for the list, we can picture it as follows, pointing to the first item in the list: Now if we call next we get back the word "apple", and the picture changes to: After two more calls to next (returning "pear" and "banana") we'll have: A call to hasNext now returns true (because there's still one more item we haven't accessed yet). A call to next returns "strawberry", and our picture looks like this: The iterator has fallen off the end of the list. Now a call to hasNext returns false, and if we call next, we'll get a NoSuchElementException.

The Iterator interface is defined in java.util, so you need to include:

at the beginning of your program in order to uses Iterators. Assuming that we've done that, and that we've added an iterator method to our List class, we can write code like the following that uses an iterator to iterate through a list L:

How to implement them?

The easiest way to implement an iterator for the List class is to define a new class (e.g., called ListIterator) with two fields:

  1. The List that's being iterated over.
  2. The index of the current item (the item the "finger" is currently pointing to).
We also define a new iterator method for the List class; that method calls the ListIterator class's constructor, passing the list itself: The ListIterator class is defined to implement Java's Iterator interface, and therefore must implement three methods: hasNext and next (both discussed above) plus an optional remove method. If you choose not to implement the remove method, you still have to define it, but it simply throws an UnsupportedOperationException. If you choose to implement the remove method, then it should remove from the list the last item returned by the iterator's next method, or should throw an IllegalStateException if the next method hasn't yet been called.

Here is code that defines the ListIterator class (note that we have chosen not to implement the remove operation):

Testing

One of the most important (but unfortunately most difficult) parts of programming is testing. A program that doesn't work as specified is not a good program, and in some contexts can even be dangerous and/or cause significant financial loss.

There are two general approaches to testing: black-box testing and white-box testing. For black-box testing, the testers know nothing about how the code is actually implemented (or if you're doing the testing yourself, you pretend that you know nothing about it). Tests are written based on what the code is supposed to do, and the tester thinks about issues like:

So if you were to do black-box testing of the List class, you should be sure to:

For white-box testing, the testers have access to the code, and the usual goal is to make sure that every line of code executes at least once. So for example, if you were to do white-box testing of the List class, you should be sure to write a test that causes the array to become full so that the expandArray method is tested (something a black-box tester may not test, since they don't even know that lists are implemented using arrays).

You will be a much better programmer if you learn to be a good tester. Therefore, teating will be an important factor in the grades you receive for the programming projects in this class.