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:
/** Interface for a list of strings */ public interface StringList { public void add(String s); public void add(int pos, String s); public int size(); public boolean isEmpty(); public int indexOf(String s); public boolean contains(String s); public String get(int pos); public void set(int pos, String s); public void remove(int pos); }
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:
final
) constants. Other than that it may not contain
any variables or other state definitions.public
keyword
is not actually required. But it's probably good style to provide it anyway.<E>
. We'll explore this later.
For now we'll stick to a simpler example using String
data objects.StringList.java
in this example.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:
ArrayStringList
, that implements an interface,
like StringList
, must provide implementations of all of
the methods defined in the interface. (We'll see that this can be relaxed
later
when we get to abstract classes, but to create a class that implements an
interface and can be instantiated, it must actually implement all of the
methods defined in the interface.) Of course the class may contain additional
methods, particularly constructors, and instance variables, either public
or private, but it must at least implement everything specified in the interface.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.
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.