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-ut
The 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
ago
Notice 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 = 850
The 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.6707776372958
The 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 abstract
The 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 interface
Because the Shape class is an abstract class, it can't be directly
instantiated, as in:
Shape s = new Shape(); // illegal
You 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); // legal
I 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.