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.
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 defintions, 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.| Triangle Interface - Triangle.class | Ex1.java |
| Point Interface - Point.class | |
~/$ java Ex1
1 2
2 3
3 4
4 5
Triangle 1.0 2.0, 2.0 3.0, 4.0 5.0 has the greatest area.
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 P, Q, V 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.
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 Strings.
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 LabPoints
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.
Important! One of the failures of procedural programming to separate interface from implementation is that it didn't allow us to extend or modify the behavior of code without messing around with its implementation. Here is an example where we see ourselves wanting to do this. We want to augment class Point by adding a label and a method for getting that label, and we want to modify how toString works for points to include those labels. And now we really feel the need for something more than procedural programming offers us, because we don't have access to Point's implementation, so we can't go into that code and actually muck around with it to add labels and change what toString does. Inheritance is the third pillar of Object Oriented Programming, and it removes this limitation!
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
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 — constructorssuper 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 methodsgetLabel(), 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 methods
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.
| LabPoint.java | Ex3.java |