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> {
public abstract 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
class 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.