Recall that there are four basic mechanisms / ideas underlying object oriented programming:
Today we start to look at inheritance, which will solve the second of our three problems with Procedural Programming not giving us full separation of interface from implementation: that you can't modify or extend the behavior of functions or collections of functions and structs without messing around with their implementations. Inheritance is a mechanism that allows us to create a new class from an existing class. It has two basic kinds of uses: to modify or extend functionality, and to express commonality. We will focus on the first use for this lesson. The second use is intimately related to polymorphism, which is the last of the four basic mechanisms/ideas behind object oriented programming.
Suppose there are existing classes Point
and Triangle
. We could
assume for this discussion that you only have their .class files, i.e. that you can't even
see their source code. I'm happy to let you see it, for educational purposes, but the point
is that we could do everything we'll talk about today without ever having access to the source
code. In any event, we'll assume we either are not allowed to change those two class definitions,
or are unable to change them (no source code), or don't want to change them (too hard to
figure out). We do, however, have their interfaces (shown below) and have used the class
to create a cool little program Ex1.java
that reads in four points, and returns
the three points of the four that define the triangle with largest area. If you want the
source code: see Point.java and Triangle.java.
public class Triangle {
public static double perimeter(Point a, Point b, Point c);
public static double area(Point a, Point b, Point c);
}
public class Point {
public Point(double x, double y);
public Point clone();
public double getX();
public double getY();
public double getAngle();
public double getRadius();
public void subtractBy(Point p);
public void multiplyBy(double d);
public void rotateCCBy(double ang);
public String toString();
public static Point mkPointFromAngleRadius(double a, double r);
public static Point read(Scanner sc);
}
import java.util.*;
public class Ex1 {
public static void main(String[] args) {
// Read in four points
Scanner sc = new Scanner(System.in);
Point[] v = new Point[4];
for(int i = 0; i < v.length; i++)
v[i] = Point.read(sc);
// Populate w with arrays for all three combinations
// of three points from v
Point[][]w = new Point[4][3];
for(int i = 0; i < 4; i++) {
for(int j = 0; j < 3; j++) {
w[i][j] = v[j + (j < i ? 0 : 1)];
}
}
// Find index m of the three points t[m][0],t[m][1],t[m][2]
// that give the largest area
int m = 0;
for(int i = 1; i < 4; i++) {
if (Triangle.area(w[m][0],w[m][1],w[m][2]) <
Triangle.area(w[i][0],w[i][1],w[i][2]))
m = i;
}
// Print out the three points that give the triangle with largest area
System.out.println("The triangle with maximum area is: ");
for(int i = 0; i < 3; i++)
System.out.println(w[m][i].toString());
}
}
Here's a sample run of the program:
~/$ java Ex1
1 2
2 3
3 4
4 5
The triangle with maximum area is:
1.0 2.0
3.0 4.0
4.0 5.0
So these are some nice classes; lots of functionality. Clearly the static
methods area
and perimeter
in the Triangle
class rely
on some of the many methods of class Point
, but we don't know which. Using these
classes, we've easily written what is actually not an entirely trivial program.
Now: suppose our goal is to have the program work so that we enter a label with each point, and print out the labels of the three points that define the triangle with maximum area, like this:
~/$ java Ex3
1 2 P
2 3 W
3 4 Q
4 5 V
Triangle PQV has the greatest area.
This can be done, of course, in a variety
of ways. So it's not that solving this little problem is all that difficult. What's interesting,
is to see how the problem can be solved easily in an object oriented way, respecting the
separation of interface and implementation, with the use of inheritance.
What we would really like to do to solve our problem is to go add a "label" field to the class Point, so that when we had chosen the three points that defined the triangle of largest area, we could simply write out their labels. However, we can't modify the class Point: first, I said we couldn't, second, what about all those situations in which we want to have points without labels? So we have to create a new class that somehow combines a point and a label.
We already have a mechanism for using existing classes to build new classes. You use it whenever
you create a class that has, for example, a String
field (data member). We
haven't given it a special name, but if you want to know, we call this composition of classes. If class Foo
has two String
fields, for instance, we
would say that Foo
is composed
of two String
s. Composition is a has-a relationship. Foo "has-a"
String field.
If we try to combine a point and a string label by composition, we get something like this:
public class LabPoint {
private Point p;
private String lab;
...
}
... which is depicted to the right. This really ugly for us. Why? Well this
is no longer a point, which means that we can't just pass three LabPoint
s to
Triangle.area
anymore, but we constantly have to pull out the "point-part" of
the LabPoint to pass around. In fact, this class has no methods yet at all, and we'd have
to recreate all the methods we already have for Point in the new LabPoint class. Either that
or, as with calling Triangle.area, we will have to constantly pull out the "point-part" whenever
we want to calculate something. This is because a LabPoint defined this way has-a Point, rather than is-a Point.
If we approach this with inheritance, we would define the new class LabPoint as
derived from the class Point — in Java class LabPoint extends Point
— which means that a LabPoint object is-as Point, but it is a Point with some
extra features. Thus, all the code that worked for Points, all the methods of class Point
and methods that take Points as arguments, all still works. But we can add more! The existing
class Point is called the "super-class" and the new class LabPoint is called the "sub-class".
extend
— adding fields
Though it goes a bit against the grain to start with the implementation, in this case it's
best to start with adding a field to store the label, as it makes what's going on a bit easier
to see. To derive a new class LabPoint from class Point, and add a String label, we would
write:
public class LabPoint extends Point {
private String lab;
}
... which gives the picture shown to the right. Since a LabPoint is-a Point, if
lp is a LabPoint we can make calls like
lp.getRadius()
or if lq is another LabPoint, calls like
lp.subtractBy(lq)
. Why? Because lp and lq are Points. They're just
Points that carry around labels. Now, before we do much of anything, though, we need to figure
out how to initialize LabPoints when they're instantiated.
extend
— constructors
Constructors require some discussion. Why? because the constructor for a LabPoint would naturally
have three values - x, y, and label lab. However, the constructor has to split duties and
say that we want the Point part of the LabPoint to be initialized with x and y, and the label
part with lab. This is accomplished with the keyword super
which, in this
context, allows us to explicitly call the constructor for the "super-class" Point to initialize
the Point part of the new object.
public class LabPoint extends Point {
private String lab;
public LabPoint(double x, double y, String lab) {
super(x,y); //← calls Point constructor with arguments x and y
this.lab = lab;
}
}
Note: Java guarantees that a constructor for the superclass will be called whenever
a new instance of the subclass is created. This means that if you don't explicitly call
super(...)
in the constructor, the compiler/JVM will implicitly execute
super()
prior to executing the statements in your constructor, where "super()
"
is a call to the superclass's "default constructor", i.e. the constructor that takes no arguments.
If there is no accessible default constructor for the superclass (as indeed is the case with
our Point
example), it is an error.
extend
— adding methods
In addition to extending Point by adding new fields, we can add new methods. In this case,
it's natural to add a new method getLabel()
, a method that wouldn't have made
sense for class Point.
public class LabPoint extends Point {
private String lab;
public LabPoint(double x, double y, String lab) {
super(x,y);
this.lab = lab;
}
public String getLabel() { return lab; }
}
extend
— overriding methodsNot only can we add new methods, we can modify the behavior of existing functions when applied to the new class LabPoint. This is called overriding a method. It's actually a special case of the function overloading we're all used to. If we add the following method definition to class LabPoint:
public String toString() {
return getX() + " " + getY() + " " + getLabel();
}
then the call System.out.println(lp.toString())
, where lp is a LabPoint, will
result in LabPoint's version of toString() being called rather than Point's version of toString(),
and what gets printed out will include the lp's label. Interestingly, we could also define
this in terms of Point's version of toString() the following way:
public String toString() {
return super.toString() + " " + getLabel();
}
... which says to return the string that Point's version of toString() would produce, concatenated with a space and the label.
We can also override static methods. This is slightly different, in the sense that the new static method really has a different name. In our example, we'll naturally want to override read(..) so that it reads in the label as well as the x and y value.
public static LabPoint read(Scanner sc) {
Point tmp = Point.read(sc);
if (tmp == null || ! sc.hasNext()) { return null; }
String lab = sc.next();
return new LabPoint(tmp.getX(),tmp.getY(),lab);
}
Once again, notice that we can make use of Point's versions of methods, as
in the explicit call to Point.read. In the simple program below:
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
LabPoint p = LabPoint.read(sc);
System.out.println(p.toString());
}
... notice how we specify which "read" we want, and notice that because p
is LabPoint, we get the LabPoint version of toString() rather than the Point version.
If you put these pieces of LabPoint together, you get the definition shown below. A simple search-and-replace of LabPoint for Point in the file Ex1.java yields a program that does almost exactly what we want. I massaged it slightly to produce the Ex3.java shown below, but that was to restrict the output too only the labels.
import java.util.*;
public class LabPoint extends Point {
public LabPoint(double x, double y, String lab) {
super(x,y);
this.lab = lab;
}
public String getLabel() { return lab; }
public static LabPoint read(Scanner sc) {
Point tmp = Point.read(sc);
if (tmp == null || ! sc.hasNext()) { return null; }
String lab = sc.next();
return new LabPoint(tmp.getX(),tmp.getY(),lab);
}
public String toString() {
return getX() + " " + getY() + " " + lab;
}
private String lab;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
LabPoint p = read(sc);
System.out.println(p);
}
}
import java.util.*;
public class Ex3 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// Read in four points
LabPoint[] v = new LabPoint[4];
for(int i = 0; i < v.length; i++) {
v[i] = LabPoint.read(sc);
}
// Populate w with arrays for all three combinations
// of three points from v
LabPoint[][] w = new LabPoint[4][3];
for(int i = 0; i < 4; i++) {
for(int j = 0; j < 3; j++) {
w[i][j] = v[j + (j < i ? 0 : 1)];
}
}
// Find index m of the three points t[m][0],t[m][1],t[m][2]
// that give the largest area
int m = 0;
for (int i = 1; i < 4; i++) {
if (Triangle.area(w[m][0],w[m][1],w[m][2]) <
Triangle.area(w[i][0],w[i][1],w[i][2]))
m = i;
}
// Print out the three points that give the triangle with largest area
System.out.print("Triangle ");
for(int i = 0; i < 3; i++) {
System.out.print(w[m][i].getLabel());
}
System.out.println(" has the greatest area.");
}
}