It is useful to study stacks and queues as a way to understand a minimal kind of data structure. We'll find, for example, that they are less powerful than the list structures we have been looking at. But we often find ourselves wanting to think in terms of the simplest possible solution to a problem, as in, "You could solve that with a stack."
Like lists, stacks and queues store an ordered sequence of values. A minimal set of operations for such a structure would require at least:
Stacks and queues are similar in that they each store a sequence of values in a particular order. But stacks are what we call LIFO structures while queues are FIFO structures:
stacks queues L-ast F-irst I-n I-n F-irst F-irst O-ut O-utThe analogy for stacks is to think of a cafeteria and how trays are stacked up. When you go to get a tray, you take the one on the top of the stack. You don't bother to try to get the one on the bottom, because you'd have to move a lot of trays to get to it. Similarly if someone brings clean trays to add to the stack, they are added on the top rather than on the bottom. The result is that stacks tend to reverse things. Each new value goes to the top of the stack, and when we take them back out, we draw from the top, so they come back out in reverse order.
The analogy for queues is to think about standing in line at the grocery store. As new people arrive, they are told to go to the back of the line. When the store is ready to help another customer, the person at the front of the line is helped. In fact, the British use the word "queue" the way we use the word "line" telling people to "queue up" or to "go to the back of the queue".
In the case of a stack, the adding operation is called "push" and the removing operation is called "pop". All operations occur at one end of the stack, at the top. We push values onto the top and we pop them off the top. There is also a method for testing whether the stack is empty and an operation for requesting the current size of the stack. So for a Stack<E>, the basic operations are:
public void push(E value); public E pop(); public boolean isEmpty(); public int size();Notice that we are using Java generics to define the Stack in terms of an unspecified element type E. That way we'll be able to have a Stack<String> or Stack<Integer> or a Stack of any other kind of element type we are interested in.
For queues, we have a corresponding set of operations but they have different names. The operations for a Queue<E> are:
public void add(E value); public E remove(); public boolean isEmpty(); public int size();The collections framework does the right thing in terms of Queue<E> by making it an interface. The implementation we will be using for actual objects is LinkedList<E>. The stack version is much older and was not done as well. In particular, Stack<E> is a class, not an interface.
I first showed this client code:
import java.util.*; public class StackQueue1 { public static void main(String[] args) { Queue<String> q = new LinkedList<String>(); Stack<String> s = new Stack<String>(); String[] data = {"four", "score", "and", "seven", "years", "ago"}; for (String str : data) { s.push(str); q.add(str); } System.out.println("stack = " + s); while (!s.isEmpty()) System.out.println(s.pop()); System.out.println("queue = " + q); while (!q.isEmpty()) System.out.println(q.remove()); } }It produces the following output:
stack = [four, score, and, seven, years, ago] ago years seven and score four queue = [four, score, and, seven, years, ago] four score and seven years agoNotice that the output for the stack lists it starting with the value at the bottom of the stack. The value at the end of the list ("ago") is at the top of the stack and that's why it is the first value popped from the stack. As you would expect, the queue lists the values in order starting at the front of the queue ("four").
Then we looked at some typical operations on a stack and queue of integers. We looked at the following methods that can be used to transfer values from a queue to a stack or from a stack to a queue:
public static void queueToStack(Queue<Integer> q, Stack<Integer> s) { while (!q.isEmpty()) { int n = q.remove(); s.push(n); } } public static void stackToQueue(Stack<Integer> s, Queue<Integer> q) { while (!s.isEmpty()) { int n = s.pop(); q.add(n); } }Then I asked people how we could write a method that would find the sum of the values in a queue. It is a cumulative sum task, which involves initializing a sum variable to 0 outside the loop and then adding each value to the sum as we progress through the loop. Our first attempt looked like this:
public static int sum(Queue<Integer> q) { int sum = 0; while (!q.isEmpty()) { int n = q.remove(); sum += n; } return sum; }When we called the method from main and printed the queue afterwards, we found that the queue is empty. As a side effect of calculating the sum, we destroyed the contents of the queue. This is generally not acceptable behavior.
Unfortunately, queues don't give us any peeking operations. We have no choice but to take things out of the queue. But we can restore the queue to its original form. How? Someone suggested using a second queue. That would work, but there is an easier way. Why not use the queue itself? As we remove values to be processed, we re-add them at the end of the list. Of course, then the queue never becomes empty. So instead of a while loop looking for an empty queue, we wrote a for loop using the size of the queue:
public static int sum(Queue<Integer> q) { int sum = 0; for (int i = 0; i < q.size(); i++) { int n = q.remove(); sum += n; q.add(n); } return sum; }By printing the queue before and after a call on sum, we were able to verify that our new version preserved the queue.
I then said I wanted to consider a variation of the sum method for stacks:
public static int sum(Stack<Integer> s) { ... }This method can also be called sum because the two methods have different signatures. Remember that a signature of a method is its name plus its parameters. These are both called sum and they both have just a single parameter, but the parameter types are different, so this is okay. This is called overloading a method and it is a common technique in Java.
So how do we write the sum method for stacks? At first I simply substituted stack operations for queue operations:
int sum = 0; for (int i = 0; i < s.size(); i++) { int n = s.pop(); sum += n; s.push(n); } return sum;Unfortunately, this code didn't work. We saw output like this when we tested it from main:
stack = [42, 19, 78, 87, 14, 41, 57, 25, 96, 85] sum = 850The sum of these numbers is not 850. We're getting that sum because the loop pops the value 85 off the stack 10 different times and then pushes it back onto the top of the stack. With a queue, values go in at one end and come out the other end. But with a stack, all the action is at one end of the structure (the top). So this approach isn't going to work.
In fact, you can't solve this in a simple way with just a stack. You'd need something extra like an auxiliary structure. I said to consider how we could solve it if we had a queue available as auxiliary storage. Then we can put things into the queue as we take them out of the stack and after we have computed the sum, we can transfer things from the queue back to the stack using our queueToStack method:
int sum = 0; Queue<Integer> q = new LinkedList<Integer>(); for (int i = 0; i < s.size(); i++) { int n = s.pop(); sum += n; q.add(n); } queueToStack(q, s); return sum;This also didn't work. Here is a sample execution:
initial stack = [32, 15, 54, 91, 47, 45, 88, 89, 13, 0] sum = 235 after sum stack = [32, 15, 54, 91, 47, 0, 13, 89, 88, 45]There are two problems here. Only half of the values were removed from the stack and those values now appear in reverse order. Why only half? We are using a for loop that compares a variable i against the size of the stack. The variable i is going up by one while the size is going down by one every time. The result is that halfway through the process, i is large enough relative to size to stop the loop. This is a case where we want a while loop instead of a for loop:
int sum = 0; Queue<Integer> q = new LinkedList<Integer>(); while (!s.isEmpty()) { int n = s.pop(); sum += n; q.add(n); } queueToStack(q, s); return sum;Even this is not correct. It finds the right sum, but it ends up reversing the values in the stack. Some people said, "Then why don't you use a stack for auxiliary storage?" That would solve the problem, but one of the things we are testing is whether you can figure out how to solve a problem like this given a certain set of tools. In this case, you are given auxiliary storage in the form of a queue.
The problem is that by transferring the data from the stack into the queue and then back into the stack, we have reversed the order. The fix is to do it again so that it goes back to the original. So we add two extra calls at the end of the method that move values from the stack back into the queue and then from the queue back into the stack:
public static int sum(Stack<Integer> s) { int sum = 0; Queue<Integer> q = new LinkedList<Integer>(); while (!s.isEmpty()) { int n = s.pop(); sum += n; q.add(n); } queueToStack(q, s); stackToQueue(s, q); queueToStack(q, s); return sum; }Then I turned to a new topic. I said that I wanted to explore how to design a set of classes for storing information about various shapes like rectangles, circles and squares. I started with these three classes:
public class Circle { private double radius; public Circle(double radius) { this.radius = radius; } public double area() { return Math.PI * radius * radius; } public String toString() { return "circle of area " + area(); } } public class Rectangle { private double length; private double width; public Rectangle(double length, double width) { this.length = length; this.width = width; } public double area() { return length * width; } public String toString() { return "rectangle of area " + area(); } } public class Square { private double length; public Square(double length) { this.length = length; } public double area() { return length * length; } public String toString() { return "square of area " + area(); } }If we were designing a complete system, we would probably have more than just an area and toString method for these, but this set of operations will be enough to explore the design issues involved. I showed the following client program that sets up an array of shapes and that attempts to sort them by calling the built-in Arrays.sort method:
import java.util.*; public class ShapeTest { public static void main(String[] args) { Object[] data = {new Square(12), new Rectangle(15, 3.2), new Circle(8.4), new Circle(1.5), new Square(8.7), new Rectangle(7.2, 3.2), new Square(2.4), new Circle(3.7), new Circle(7.9)}; for (Object s : data) System.out.println(s); System.out.println(); Arrays.sort(data); for (Object s : data) System.out.println(s); } }The array stores a combination of Circle, Square and Rectangle objects, which is why we had to declare it to be of type Object[]. The client code uses a foreach loop to print the shapes before and after the call on Arrays.sort:
for (Object s : data) System.out.println(s);When we executed this program, it properly printed the various shapes but then it reached an execution error when we called Arrays.sort. The error message reported was ClassCastException for the Square class. The problem is that we never had our shape classes implement the Comparable interface. So we opened up the Square class and tried to figure out how to make it implement Comparable.
First we had to change its header:
public class Square implements Comparable<Square> { ... }Then we had to think about how to write an appropriate compareTo method. It's a bit tricky because it involves the difference of two doubles. So we can't just say:
public int compareTo(Square other) { return area() - other.area(); }We could cast to an int, but even that doesn't work:
public int compareTo(Square other) { return (int) (area() - other.area()); }This returns an int, but it returns the wrong value when the difference is something like 0.5 or -0.3, both of which would be turned to 0 when you cast to int. Instead we need a 3-way comparison:
public int compareTo(Square other) { double difference = area() - other.area(); if (difference < 0) return -1; else if (difference == 0) return 0; else // difference > 0 return 1; }We tried compiling with this definition and we ran the client program again. It failed again, but this time it failed on a ClassCastException on Rectangle. That's because we only modified the Square class, not the Rectangle class. We discussed the possibility of copying the compareTo method to those other classes, but someone pointed out a problem with our definition. We have defined a compareTo method that allows a Square to compare itself to another Square, but we need one that allows a Square to compare itself to any shape, including a Circle or Rectangle.
Someone suggested that we could have a Shape interface that they all implement:
public interface Shape { }We then modified our Square class header and the compareTo method:
public class Square implements Comparable<Shape> { ... public int compareTo(Shape other) { double difference = area() - other.area(); if (difference < 0) return -1; else if (difference == 0) return 0; else // difference > 0 return 1; } }But this code didn't compile. We got a complaint that it couldn't find the method area(). This seemed odd, because each of the three classes has an area method. But remember from our discussion about inheritance that Java has a notion of a "role". The Shape role does not include an area method unless we specifically say that it does.
public interface Shape { public double area(); }With this change, the Square class compiled properly. Then I said that we want to copy whatever pattern we use for the Square class in each of the other classes. But before we went to do that, I wanted to make sure that we had the right pattern. So again looked closely at the Square class:
public class Square implements Comparable<Shape> { private double length; public Square(double length) { this.length = length; } public double area() { return length * length; } public String toString() { return "square of area " + area(); } public int compareTo(Shape other) { double difference = area() - other.area(); if (difference < 0) return -1; else if (difference == 0) return 0; else // difference > 0 return 1; } }I said that we're missing a key ingredient. Someone mentioned that we never established the "is a" relationship to say that a Square is a Shape. So we had to modify the header to indicate that:
public class Square implements Shape, Comparable<Shape> { ... }We could have repeated this pattern in each of the other classes, but it seems somewhat redundant to always have to say that a class implements both Shape and Comparable<Shape>. And we don't want to have some Shape classes that fail to implement the Comparable interface. Java provides us a nice alternative. We can elevate this restriction to the Shape interface itself:
public interface Shape extends Comparable<Shape>{ public double area(); }It may seem odd that use the keyword "extends" rather than the keyword "implements." That's because in this case we have an interface extending another interface and in Java you specify that with the keyword "extends" rather than the keyword "implements."
This allowed us to simplify the Square class header:
public class Square implements Shape { ... }We then changed each of the different shape classes to say that they implement this interface:
public class Circle implements Shape { ... } public class Rectangle implements Shape { ... } public class Square implements Shape { ... }With this change, we were able to compile the Square class. We then copied the compareTo method to the other two classes and compiled them. This allowed the client program to run properly, producing this output:
square of area 144.0 rectangle of area 48.0 circle of area 221.6707776372958 circle of area 7.0685834705770345 square of area 75.68999999999998 rectangle of area 23.040000000000003 square of area 5.76 circle of area 43.00840342764427 circle of area 196.066797510539 square of area 5.76 circle of area 7.0685834705770345 rectangle of area 23.040000000000003 circle of area 43.00840342764427 rectangle of area 48.0 square of area 75.68999999999998 square of area 144.0 circle of area 196.066797510539 circle of area 221.6707776372958The program correctly sorts the shapes into increasing area. In fact, given the new Shape interface we were able to rewrite the main program to replace "Object" with "Shape":
Shape[] data = {new Square(12), new Rectangle(15, 3.2), new Circle(8.4), new Circle(1.5), new Square(8.7), new Rectangle(7.2, 3.2), new Square(2.4), new Circle(3.7), new Circle(7.9)}; for (Shape s : data) System.out.println(s); System.out.println(); Arrays.sort(data); for (Shape s : data) System.out.println(s);But this still wasn't very satisfying because we have three copies of the compareTo method, one in each of the different shape classes. This kind of redundancy is a bad idea. When we first talked about inheritance, we talked about the idea of having a 20-page employee manual that is shared across all types of employees. We want something like that here.
Someone suggested that we change the Shape interface to a class and move the method there:
public class Shape implements Comparable<Shape> { public double area(); public int compareTo(Shape other) { double difference = area() - other.area(); if (difference < 0) return -1; else if (difference == 0) return 0; else // difference > 0 return 1; } }Unfortunately, this didn't compile. It produced the following error message:
Error: missing method body, or declare abstractThe problem is with the area method. Currently we just have a semicolon rather than a body defining what it should do. But the definitions of the area methods are in the various shape classes (Circle, Square, Rectangle). Someone suggested deleting it from the class, but then it didn't compile because the compareTo method calls area. So we have to define it here in some way.
We considered the possibility of giving it a "dummy" definition just to make it compile:
public double area() { return 42.42; }We just made up a number for it to return. This allowed us to compile the Shape class, but it's not a good idea. It means that someone might extend the Shape class and inherit this strange version of the area method. It's better to leave the shape method unspecified, the way it was with the interface.
Someone pointed out that the original error message seemed to be giving us a suggestion. It said that we could declare it to be "abstract". The keyword "abstract" is a modifier that can be applied to methods and classes like the keywords "public" and "static", so we just added this extra modifier to the method header:
public class Shape implements Comparable<Shape> { abstract public double area(); public int compareTo(Shape other) { ... } }That also didn't work. The compiler pointed at the class header and said, "Shape is not abstract and does not override abstract method area() in Shape." In other words, the compiler is saying that if you are going to have an abstract method called area in the Shape class, then the class itself has to be declared abstract:
public abstract class Shape implements Comparable<Shape> { public abstract double area(); public int compareTo(Shape other) { ... } }After this change the class finally compiled. We then had to change the individual shape classes to say that they extend this class rather than implementing an interface called Shape.
At last we had arrived at the name of the topic for the lecture: abstract classes. We've seen two extremes in Java. A "normal" class (often called a concrete class) is one where every method has a definition. An interface is like a purely abstract class where every method is abstract. In other words, in an interface we list only method headers (methods that need to be filled in). An abstract class sits somewhere in the middle, typically having some methods that are filled in and some methods that are abstract. You can think of this as a spectrum from concrete to purely abstract, with abstract classes in the middle:
concrete <---+-----------------------+------------------------+----> abstract | | | concrete class abstract class interfaceBecause the Shape class is an abstract class, it can't be directly instantiated, as in:
Shape s = new Shape(); // illegalYou aren't allowed to create instances of an abstract class. But that doesn't mean you can't have variables of type Shape, as in:
Shape s = new Rectangle(20, 30); // legalI mentioned that this is similar to the way that we use the word "Employee". We know that everyone who works for a company is in some sense an employee, but you wouldn't find someone who is just an employee. If you ask someone, "What do you do?" they won't answer, "I'm an employee." If they did, you'd say, "I know, but what do you do?" The word "employee" is like an abstract class. It is a useful concept in creating our hierarchies and it's useful to talk about people as employees, but there aren't any people who are actually just plain employees.
In the last few minutes of class I pointed out that there was still some redundancy between these three classes. They each have a toString method that is very similar, as in the Circle class version:
public String toString() { return "circle of area " + area(); }The only difference between the three versions is the name of the shape. We talked about how to move this up into the Shape class. To do so, we need a new field to keep track of the name of the shape:
public String toString() { return name + " of area " + area(); }We modified the Shape class to have this as a field that is set by a call on a Shape class constructor:
public abstract class Shape implements Comparable<Shape> { private String name; public Shape(String name) { this.name = name; } ... }This required us to include calls on the "super" constructor in each of our Shape classes, as in this version for Circle:
public Circle(double radius) { super("circle"); this.radius = radius; }I briefly mentioned one last detail. You can include the access modifier "final" to a method or class to prevent it from being overridden through inheritance. The final version of the shape hierarchy appears in the handout that I distributed and it makes both the compareTo and toString methods final, which means that subclasses can't override this shared behavior of shape objects. This can be useful to prevent malicious or careless errors. For example, you wouldn't want to have most of your shape objects comparing themselves in one way while some others are comparing in some other way. You also wouldn't want to let a shape subclass modify the toString method because then it could masquerade as something other than what it is.