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).
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.
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.
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:
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:
Question 1.
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.
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:
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:
The Iterator interface is defined in java.util,
so you need to include:
The easiest way to implement an iterator for the List class
is to define a new class (e.g., called ListIterator)
with two fields:
Here is code that defines the ListIterator class (note
that we have chosen not to implement the remove operation):
Array Implementation
public class List {
// *** fields ***
private Object[] items; // the items in the list
private int numItems; // the number of items in the list
//*** methods ***
// constructor
public List() { ... }
// add items
public void add(Object ob) { ... }
public void add(int pos, Object ob) { ... }
// remove items
public Object remove(int pos) { ... }
// get items
public Object get (int pos) { ... }
// other methods
public boolean contains Object(ob) { ... }
public int size() { ... }
public boolean isEmpty() { ... }
}
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
Then we can add the new item to the end of the list.
//**********************************************************************
// add
//
// Given: Object ob
//
// Do: Add ob to the end of the list
//
// Implementation:
// If the array is full, replace it with a new, larger array;
// store the new item after the last item
// and increment the count of the number of items in the list.
//**********************************************************************
public void add(Object ob) {
// if array is full, get new array of double size,
// and copy items from old array to new array
if (items.length == numItems) expandArray();
// add new item; update numItems
items[numItems] = ob;
numItems++;
}
//**********************************************************************
// expandArray
//
// Do:
// o Get a new array of twice the current size.
// o Copy the items from the old array to the new array.
// o Make the new array be this list's "items" array.
//**********************************************************************
private void expandArray() {
Object[] newArray = new Object[numItems*2];
for (int k=0; k<numItems; k++) {
newArray[k] = items[k];
}
items = newArray;
}
//**********************************************************************
// add
//
// Given: int pos, Object ob
//
// Do: Add ob to the list in position pos (moving items over to the right
// to make room).
//
// Exceptions:
// Throw IndexOutOfBoundsException if pos<0 or pos>numItems
//
// Implementation:
// 1. check for bad pos
// 2. if the array is full, replace it with a new, larger array
// 3. move items over to the right
// 4. store the new item in position pos
// 5. increment the count of the number of items in the list
// **********************************************************************
public void add(int pos, Object ob) {
// check for bad pos and for full array
if (pos < 0 || pos > numItems) throw new IndexOutOfBoundsException();
if (items.length == numItems) expandArray();
// move items over and insert new item
for (int k=numItems; k>pos; k--) {
items[k] = items[k-1];
}
items[pos] = ob;
numItems++;
}
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.
private static final int INITSIZE = 10;
//**********************************************************************
// List constructor
//
// initialize the list to be empty
//**********************************************************************
public List() {
numItems = 0;
items = new Object[INITSIZE];
}
Iterators
What are they?
for (int k=0; k<L.size(); k++) {
Object ob = L.get(k);
... do something to ob here ...
}
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.
apple pear banana strawberry
If we create an Iterator for the list, we can picture it as follows,
pointing to the first item in the list:
apple pear banana strawberry
^
|
Now if we call next we get back the word "apple", and the
picture changes to:
apple pear banana strawberry
^
|
After two more calls to next (returning "pear" and "banana")
we'll have:
apple pear banana strawberry
^
|
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:
apple pear banana strawberry
^
|
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.
import java.util.*;
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:
Iterator it = L.iterator();
while (it.hasNext()) {
Object ob = it.next();
... do something to ob here ...
}
How to implement them?
We also define a new iterator method for the List class;
that method calls the ListIterator class's constructor, passing
the list itself:
//**********************************************************************
// iterator
//
// return an iterator for this list
//**********************************************************************
public Iterator iterator() {
return new ListIterator(this);
}
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.
public class ListIterator implements Iterator {
// *** fields ***
private List myList;
private int myPos;
//*** methods ***
// constructor
public ListIterator(List L) {
myList = L;
myPos = 0;
}
public boolean hasNext() {
return (myPos < myList.size());
}
public Object next() {
if (!hasNext()) throw new NoSuchElementException();
myPos++;
return (myList.get(myPos-1));
}
public void remove() {
throw new UnsupportedOperationException();
}
}