CSE143 Notes 4/17/06

Interfaces; Stacks & Queues

We've now seen two very different implementations of lists of strings: StringList, which used an array to store the list elements, and LinkedStringList, which used a linked list. While these were different classes with different implementations, from the client's perspective they were basically the same. Both provided the same set of operations: one or more constructors, and a collection of methods including add(s), add(pos, s), size(), isEmpty(), contains(s), indexOf(s), get(pos), set(pos, s), and remove(pos). The main differences were in the efficiency of the various operations in the different implementations.

Whenever two or more things are very similar, a good programmer will look to see if there's some way to abstract out the similarities and define a common abstraction that captures the common behavior. This idea is so important that modern programming languages provide explicit support for it. In Java, the language mechanism that allows us to specify an abstraction is the interface.

For our example, we can define the following interface to provide the common specification for both the array-based and linked-list-based versions of StringList. The code would look something like this:

The interface would also normally contain JavaDoc comments for each of the methods, but those are omitted here to save space and to focus on the technical details of interfaces.

A few points about interfaces:

Given the concept of an interface, we want to look at it from two perspectives. One is the implementer's view - the programmer who is going to create a specific class that meets the specification. The other is the client's view - the person using classes that implement the interface. We'll look at the implementer's view first.

From the implementer's perspective, we can specify that a class is an implementation of an interface with the implements keyword. Both our original StringList class (renamed ArrayStringList from now on) and the LinkedStringList class can be specified as implementations of the StringList interface given above.

   /** Array-based implementation of StringList */
   public class ArrayStringList implements StringList {
     ... contents as before ...
   }
 
   /** Linked-list-based implementation of StringList */
   public class LinkedStringList implements StringList {
     ... contents as before ...
   }

The implements clause has several implications, the most important of which are:

From the client's point of view, both the interface and the class that implements it define new types, and both can be legally used as types of variables and parameters in code. But there are strong reasons to prefer using interface types instead of class names wherever possible. In his book, Effective Java, Joshua Bloch makes this point in boldface, and it's hard to imagine saying it better, so we quote it here: If appropriate interface types exist, parameters, return values, variables, and fields should all be declared using interface types. The advantage is that the resulting code will work properly with any implementation of the interface, both those that exist now, and ones that are created in the future. In fact, about the only essential reason to use the name of a class when a suitable interface exists is to write the code that creates a new object.

   // good
   StringList lst1 = new ArrayStringList();
   StringList lst2 = new LinkedStringList();
   // bad - don't do this normally
   ArrayStringList  lst1 = new ArrayStringList();
   LinkedStringList lst2 = new LinkedStringList();

Code that uses interface types extensively is much more flexible and adaptable.

Stacks and Queues

The list types we've looked at give us a general way of storing an ordered collection of data with the ability to access, add, and delete elements anywhere, although with different performance, depending on the particular list structure we're using.

Stacks and queues are also ordered (linear) collections, but they are restricted in how elements can be added, removed, and accessed. A stack provides LIFO access - Last In First Out. A common example in the physical world is a stack of trays in a cafeteria. New trays are added to the stack by placing them on top. The last tray added is the first removed. Furthermore, arbitrary access to a tray in the middle or the bottom is generally not feasible.

As a data structure, we can specify the interface to a stack of strings as follows:

   /** A stack of strings */
   public interface StringStack {

     /** push a new item onto the top of the stack */
     public void push(String s);

     /** 
      * return the top element of the stack and remove it.
      * pre: stack is not empty
      */
     public String pop();

     /**
      * return a copy of the top of the stack but do not remove it.
      * pre: stack is not empty
      */
	 public String top();
	 
     /** return true if the stack is empty, otherwise false */
     public boolean isEmpty();
   }

A queue provides FIFO access - First In First Out. The common example of this in the physical world is a waiting line of any sort - we're normally pretty upset if persons in the line are not served in the order they arrive. As a data structure, a Queue has the following interface.

   /** A queue of strings */
   public interface StringQueue {

     /** add a new item to the end of the queue */
     public void enqueue(String s);

     /** 
      * return the front element of the queue and remove it.
      * pre: queue is not empty
      */
     public String dequeue();
	 
     /** return true if the queue is empty, otherwise false */
     public boolean isEmpty();
   }

A queue may also have methods to access the front or rear elements of the queue without removing them.

As with lists, there are many possible ways to implement stacks and queues. Linked lists work well for both. An array implementation of a stack is very straightforward. Array implementations of queues are a bit trickier if we want the operations to be fast, since adding or removing items at the front of an array can be expensive. One array-based queue implementation that is fairly common is a bounded or fixed-size queue where items are added to one end of the array and removed from the other. When the end of the array is reached, we "wrap around" and store new elements at the front again, provided there is room.