The Way of the Java/Stacks
Abstract data types
abstract data typeseeADT ADT encapsulation
The data types we have looked at so far are all concrete, in the sense that we have completely specified how they are implemented. For example, the Card class represents a card using two integers. As I discussed at the time, that is not the only way to represent a card; there are many alternative implementations.
An abstract data type, or ADT, specifies a set of operations (or methods) and the semantics of the operations (what they do) but it does not specify the implementation of the operations. That's what makes it abstract.
Why is that useful?
It simplifies the task of specifying an algorithm if you can denote the operations you need without having to think at the same time about how the operations are performed.
Since there are usually many ways to implement an ADT, it might be useful to write an algorithm that can be used with any of the possible implementations.
Well-known ADTs, like the Stack ADT in this chapter, are often implemented in standard libraries so they can be written once and used by many programmers.
The operations on ADTs provide a common high-level language for specifying and talking about algorithms.
When we talk about ADTs, we often distinguish the code that uses the ADT, called the client code, from the code that implements the ADT, called provider code because it provides a standard set of services.
The Stack ADT
stack collection ADT!Stack
In this chapter we will look at one common ADT, the stack. A stack is a collection, meaning that it is a data structure that contains multiple elements. Other collections we have seen include arrays and lists.
As I said, an ADT is defined by a set of operations. Stacks can perform the following set of operations:
[constructor:] Create a new, empty stack.
[push:] Add a new item to the stack.
[pop:] Remove and return an item from the stack. The item that is returned is always the last one that was added.
[empty:] Check whether the stack is empty.
A stack is sometimes called a ``last in, first out, or LIFO data structure, because the last item added is the first to be removed.
The Java Stack Object
Stack class!Stack generic data structure data structure!generic
Java provides a built-in object type called Stack that implements the Stack ADT. You should make some effort to keep these two things---the ADT and the Java implementation---straight. Before using the Stack class, we have to import it from java.util.
Then the syntax for constructing a new Stack is
Stack stack = new Stack ();
Initially the stack is empty, as we can confirm with the empty method, which returns a boolean:
System.out.println (stack.empty ());
A stack is a generic data structure, which means that we can add any type of item to it. In the Java implementation, though, we can only add object types. For our first example, we'll use Node objects, as defined in the previous chapter. Let's start by creating and printing a short list.
LinkedList list = new LinkedList (); list.addFirst (3); list.addFirst (2); list.addFirst (1); list.print ();
The output is (1, 2, 3). To put a Node object onto the stack, use the push method:
verbatim stack.push (list.head); verbatim
The following loop traverses the list and pushes all the nodes onto the stack:
for (Node node = list.head; node != null; node = node.next) stack.push (node);
We can remove an element from the stack with the pop method.
Object obj = stack.pop ();
The return type from pop is Object! That's because the stack implementation doesn't really know the type of the objects it contains. When we pushed the Node objects, they were automatically converted to Objects. When we get them back from the stack, we have to cast them back to Nodes.
verbatim Node node = (Node) obj; System.out.println (node); verbatim
Unfortunately, the burden falls on the programmer to keep track of the objects in the stack and cast them back to the right type when they are removed. If you try to cast an object to the wrong type, you get a ClassCastException.
The following loop is a common idiom for popping all the elements from a stack, stopping when it is empty:
while (!stack.empty ()) Node node = (Node) stack.pop (); System.out.print (node + " ");
The output is 3 2 1. In other words, we just used a stack to print the elements of a list backwards! Granted, it's not the standard format for printing a list, but using a stack it was remarkably easy to do.
You should compare this code to the implementations of printBackward in the previous chapter. There is a natural parallel between the recursive version of printBackward and the stack algorithm here. The difference is that printBackward uses the run-time stack to keep track of the nodes while it traverses the list, and then prints them on the way back from the recursion. The stack algorithm does the same thing, just using a Stack object instead of the run-time stack.
wrapper class class!wrapper object type primitive type
For every primitive type in Java, there is a built-in object type called a wrapper class. For example, the wrapper class for int is called Integer; for double it is called Double.
Wrapper classes are useful for several reasons:
You can instantiate wrapper classes and create objects that contain primitive values. In other words, you can wrap a primitive value up in an object, which is useful if you want to invoke a method that requires an object type.
Each wrapper class contains special values (like the minimum and maximum values for the type), and methods that are useful for converting between types.
Creating wrapper objects
The most straightforward way to create a wrapper object is to use its constructor:
Integer i = new Integer (17); Double d = new Double (3.14159); Character c = new Character ('b');
Technically String is not a wrapper class, because there is no corresponding primitive type, but the syntax for creating a String object is the same:
String s = new String ("fred");
On the other hand, no one ever uses the constructor for String objects, because you can get the same effect with a simple String value:
String s = "fred";
Creating more wrapper objects
Some of the wrapper classes have a second constructor that takes a String as an argument and tries to convert to the appropriate type. For example:
Integer i = new Integer ("17"); Double d = new Double ("3.14159");
The type conversion process is not very robust. For example, if the Strings are not in the right format, they will cause a NumberFormatException. Any non-numeric character in the String, including a space, will cause the conversion to fail.
Integer i = new Integer ("17.1"); // WRONG!! Double d = new Double ("3.1459 "); // WRONG!!
It is usually a good idea to check the format of the String before you try to convert it.
Getting the values out
wrapper class!extracting value
Java knows how to print wrapper objects, so the easiest way to extract a value is just to print the object:
Integer i = new Integer (17); Double d = new Double (3.14159); System.out.println (i); System.out.println (d);
Alternatively, you can use the toString method to convert the contents of the wrapper object to a String
String istring = i.toString(); String dstring = d.toString();
Finally, if you just want to extract the primitive value from the object, there is an object method in each wrapper class that does the job:
int iprim = i.intValue (); double dprim = d.doubleValue ();
There are also methods for converting wrapper objects into different primitive types. You should check out the documentation for each wrapper class to see what is available.
Useful methods in the wrapper classes
As I mentioned, the wrapper classes contain useful methods that pertain to each type. For example, the Character class contains lots of methods for converting characters to upper and lower case, and for checking whether a character is a number, letter, or symbol.
The String class also contains methods for converting to upper and lower case. Keep in mind, though, that they are functions, not modifiers (see Section immutable).
As another example, the Integer class contains methods for interpreting and printing integers in different bases. If you have a String that contains a number in base 6, you can convert to base 10 using parseInt.
String base6 = "12345"; int base10 = Integer.parseInt (base6, 6); System.out.println (base10);
Since parseInt is a class method, you invoke it by naming the class and the method in dot notation.
Base 6 might not be all that useful, but hexadecimal (base 16) and octal (base 8) are common for computer science related things.
postfix infix expressions
In most programming languages, mathematical expressions are written with the operator between the two operands, as in 1+2. This format is called infix. An alternate format used by some calculators is called postfix. In postfix, the operator follows the operands, as in 1 2+.
The reason postfix is sometimes useful is that there is a natural way to evaluate a postfix expression using a stack.
Starting at the beginning of the expression, get one term (operator or operand) at a time.
If the term is an operand, push it on the stack.
If the term is an operator, pop two operands off the stack, perform the operation on them, and push the result back on the stack.
When we get to the end of the expression, there should be exactly one operand left on the stack. That operand is the result.
As an exercise, apply this algorithm to the expression 1 2 + 3 *.
This example demonstrates one of the advantages of postfix: there is no need to use parentheses to control the order of operations. To get the same result in infix, we would have to write (1 + 2) * 3. As an exercise, write a postfix expression that is equivalent to 1 + 2 * 3?
parse token delimiter class!StringTokenizer StringTokenizer class
In order to implement the algorithm from the previous section, we need to be able to traverse a string and break it into operands and operators. This process is an example of parsing, and the results---the individual chunks of the string---are called tokens.
Java provides a built-in class called a StringTokenizer that parses strings and breaks them into tokens. To use it, you have to import it from java.util.
In its simplest form, the StringTokenizer uses spaces to mark the boundaries between tokens. A character that marks a boundary is called a delimiter.
We can create a StringTokenizer in the usual way, passing as an argument the string we want to parse.
StringTokenizer st = new StringTokenizer ("Here are four tokens.");
The following loop is a standard idiom for extracting the tokens from a StringTokenizer.
while (st.hasMoreTokens ()) System.out.println (st.nextToken());
The output is
verbatim Here are four tokens. verbatim
For parsing expressions, we have the option of specifying additional characters that will be used as delimiters:
StringTokenizer st = new StringTokenizer ("11 22+33*", " +-*/");
The second argument is a String that contains all the characters that will be used as delimiters. Now the output is:
verbatim 11 22 33 verbatim
This succeeds at extracting all the operands but we have lost the operators. Fortunately, there is one more option for StringTokenizers.
StringTokenizer st = new StringTokenizer ("11 22+33*", " +-*/", true);
The third argument says, ``Yes, we would like to treat the delimiters as tokens. Now the output is
22 + 33
This is just the stream of tokens we would like for evaluating this expression.
One of the fundamental goals of an ADT is to separate the interests of the provider, who writes the code that implements the ADT, and the client, who uses the ADT. The provider only has to worry about whether the implementation is correct---in accord with the specification of the ADT---and not how it will be used.
Conversely, the client assumes that the implementation of the ADT is correct and doesn't worry about the details. When you are using one of Java's built-in classes, you have the luxury of thinking exclusively as a client.
When you implement an ADT, on the other hand, you also have to write client code to test it. In that case, you sometimes have to think carefully about which role you are playing at a given instant.
In the next few sections we will switch gears and look at one way of implementing the Stack ADT, using an array. Start thinking like a provider.
Array implementation of the Stack ADT
implementation!Stack stack!array implementation
The instance variables for this implementation are an array of Objects, which will contain the items on the stack, and an integer index which will keep track of the next available space in the array. Initially, the array is empty and the index is 0.
To add an element to the stack (push), we'll copy a reference to it onto the stack and increment the index. To remove an element (pop) we have to decrement the index first and then copy the element out.
Here is the class definition:
verbatim public class Stack
Object array; int index;
public Stack () this.array = new Object; this.index = 0;
As usual, once we have chosen the instance variables, it is a mechanical process to write a constructor. For now, the default size is 128 items. Later we will consider better ways of handling this.
Checking for an empty stack is trivial.
public boolean empty () return index == 0;
It is important to remember, though, that the number of elements in the stack is not the same as the size of the array. Initially the size is 128, but the number of elements is 0.
The implementations of push and pop follow naturally from the specification.
public void push (Object item) array[index] = item; index++;
public Object pop () index--; return array[index];
To test these methods, we can take advantage of the client code we used to exercise the built-in Stack. All we have to do is comment out the line import java.util.Stack. Then, instead of using the stack implementation from java.util the program will use the implementation we just wrote.
If everything goes according to plan, the program should work without any additional changes. Again, one of the strengths of using an ADT is that you can change implementations without changing client code.
array!resizing ArrayIndexOutOfBounds exception!ArrayIndexOutOfBounds
A weakness of this implementation is that it chooses an arbitrary size for the array when the Stack is created. If the user pushes more than 128 items onto the stack, it will cause an ArrayIndexOutOfBounds exception.
An alternative is to let the client code specify the size of the array. This alleviates the problem, but it requires the client to know ahead of time how many items are needed, and that is not always possible.
A better solution is to check whether the array is full and make it bigger when necessary. Since we have no idea how big the array needs to be, it is a reasonable strategy to start with a small size and double it each time it overflows.
Here's the improved version of push:
public void push (Object item) if (full ()) resize ();
// at this point we can prove that index < array.length
array[index] = item; index++;
Before putting the new item in the array, we check if the array is full. If so, we invoke resize. After the if statement, we know that either (1) there was room in the array, or (2) the array has been resized and there is room. If full and resize are correct, then we can prove that index < array.length, and therefore the next statement cannot cause an exception.
Now all we have to do is implement full and resize.
private boolean full () return index == array.length;
private void resize () Object newArray = new Object[array.length * 2];
// we assume that the old array is full for (int i=0; i<array.length; i++) newArray[i] = array[i]; array = newArray;
Both methods are declared private, which means that they cannot be invoked from another class, only from within this one. This is acceptable, since there is no reason for client code to use these functions, and desirable, since it enforces the boundary between the implementation and the client.
method!private private method
The implementation of full is trivial; it just checks whether the index has gone beyond the range of valid indices.
The implementation of resize is straightforward, with the caveat that it assumes that the old array is full. In other words, that assumption is a precondition of this method. It is easy to see that this precondition is satisfied, since the only way resize is invoked is if full returns true, which can only happen if index == array.length.
At the end of resize, we replace the old array with the new one (causing the old to be garbage collected). The new array.length is twice as big as the old, and index hasn't changed, so now it must be true that index < array.length. This assertion is a postcondition of resize: something that must be true when the method is complete (as long as its preconditions were satisfied).
Preconditions, postconditions, and invariants are useful tools for analyzing programs and demonstrating their correctness. In this example I have demonstrated a programming style that facilitates program analysis and a style of documentation that helps demonstrate correctness.
precondition postcondition invariant
ADT client provider wrapper class infix postfix parse token delimiter predicate postcondition
[abstract data type (ADT):] A data type (usually a collection of objects) that is defined by a set of operations, but that can be implemented in a variety of ways.
[client:] A program that uses an ADT (or the person who wrote the program).
[provider:] The code that implements an ADT (or the person who wrote it).
[wrapper class:] One of the Java classes, like Double and Integer that provide objects to contain primitive types, and methods that operate on primitives.
[private:] A Java keyword that indicates that a method or instance variable cannot be accessed from outside the current class definition.
[infix:] A way of writing mathematical expressions with the operators between the operands.
[postfix:] A way of writing mathematical expressions with the operators after the operands.
[parse:] To read a string of characters or tokens and analyze their grammatical structure.
[token:] A set of characters that are treated as a unit for purposes of parsing, like the words in a natural language.
[delimiter:] A character that is used to separate tokens, like the punctuation in a natural language.
[predicate:] A mathematical statement that is either true or false.
[postcondition:] A predicate that must be true at the end of a method (provided that the preconditions were true at the beginning).