Lecture 17

Inheritance

Inheritance is a language mechanism that allows one class to inherit the features and capabilities of another class.  We have seen an example of this previously.  The OutputStream class in the java.io package represents a destination where binary output can be written, such as a file or terminal window.  The PrintStream class extends the capabilities of the OutputStream class by adding the capability to print text consisting of characters and strings.  You have already used the PrintStream class because it is the type of the static fields System.out and System.err.

In this example, OutputStream is the base class, or superclassPrintStream is the derived class, or subclass.  We commonly denote an inheritance relationship visually by drawing each class (along with its instance variables and methods) in a box.  An arrow represents an inheritance relationship, with the arrow starting at the subclass and pointing to the superclass:

These diagrams are called class diagrams and are defined in a standard called UML, the Unified Modeling Language.  They are useful for understanding the relationships between classes at a high level.  In the diagram above we can see that the superclass OutputStream defines several methods for writing binary (byte) data, while the PrintStream extends the capabilities of OutputStream by adding several methods that print text output.  What is important to realize is that an instance of the PrintStream object has all of the basic capabilities of OutputStream, plus the additional capabilities defined by its own methods.

Is-A Relationships and Polymorphism

Inheritance defines an "Is-A" relationship between the subclass and the superclass:

An instance of the subclass Is An instance of the superclass

Java (and other object-oriented languages) take this meaning literally: any context in a program where an instance of a superclass would be allowed, it is legal to use an instance of a subclass.  For example, the following statements are legal in Java:

PrintStream ps = System.out;
OutputStream os = ps; // Assigning a PrintStream reference to an OutputStream variable!

The statements above are legal because an instance of PrintStream is an instance of OutputStream.  Other things that you can do with an instance of a subclass include

  1. Pass it to a method that expects an instance of the superclass as a parameter

  2. Return it from a method whose return type is the superclass

  3. Assign it to an element of an array whose element type is the superclass

This phenomenon (using a subclass where the superclass is expected) is known as polymorphism, and is pervasive in Java.  In fact, whenever you see a reference to a class X, you should think to yourself that it stands not only for X but also for the set of all subclasses of X.

Overriding

Quite often, a subclass will need to specialize the behavior of a method defined in the superclass.  It can do so by overriding the method in order to define a behavior for that method that is different than the one defined in the superclass.

As an example, say we have a class called Face where instances of the class can draw a face in a graphics window.  We can use overriding to define several subclasses of Face to draw different kinds of faces:

The superclass, Face, has instance variables representing the x and y coordinates of the center of the face, the size of the face, and the color in which the face should be drawn.

The kinds of faces drawn by these classes are all similar.  However, one detail differs: the mouth.  We can capture the similar aspects of each face by defining methods to draw those features, and use method overriding to allow the subclasses to redefine how the mouth is drawn.

The Face class [source code] is the superclass.  It defines the instance variables specifying the location, size, and color of the face, as well as the default face-drawing methods.  In particular, it defines the main draw method for drawing a face in a graphics window, as well as several auxiliary methods to draw different parts of a face:

  public void draw(Graphics graphics) {
    graphics.setColor(color);
    drawCircle(graphics);
    drawEyes(graphics);
    drawMouth(graphics);
  }
  
  protected void drawCircle(Graphics graphics) {
    graphics.drawOval(x - (size/2), y - (size/2),
                      size, size);
  }
  
  protected void drawEyes(Graphics graphics) {
    int len = size / 4;
    graphics.fillOval(x - len, y - len,
                      6, 6);
    graphics.fillOval(x + len, y - len,
                      6, 6);
  }
  
  protected void drawMouth(Graphics graphics) {
    graphics.drawLine(x - (size/4), y + (size/4),
                      x + (size/4), y + (size/4));
  }

Based on the location, size, and color instance variables, the draw method will draw a face (with a neutral expression) in a window using the Graphics object passed as a parameter.

Note that the three auxiliary methods---drawCircle, drawEyes, and drawMouth---are all declared as protected.  The protected keywords means that the specified method or variable is visible to subclasses, but not visible to "unrelated" classes.  By declaring these methods as protected, we will allow the subclasses to override them, but not allow unrelated classes to call them directly.

Now let's examine the complete source code for the SurprisedFace class [source code]:

import java.awt.Color;
import java.awt.Graphics;

public class SurprisedFace extends Face {
  // inherit instance variables from base class
  
  // constructor
  public SurprisedFace(int x_, int y_, int size_,
                       Color color_) {
    // invoke constructor of base class
    super(x_, y_, size_, color_);
  }
  
  protected void drawMouth(Graphics graphics) {
    graphics.drawOval(getX() - (getSize()/4),
                      getY(),
                      getSize()/2,
                      getSize()/4);
  }
}

There are a number of important things going on here.

First, the extends keyword is used to specify that SurprisedFace will use Face as its superclass.

Usually, a constructor will assign initial values to the new object's instance variables.  However, SurprisedFace's instance variables are inherited from the superclass Face, and Face declares them to be private, meaning that SurprisedFace can't access them directly.  This is actually a good thing: in general, methods in a subclass should not directly access instance variables declared in the superclass: they should use accessor methods, just as any other class would.  So, SurprisedFace's constructor invokes the superclass constructor using the super keyword.  This ensures that the values of the parameters of SurprisedFace's constructor are passed directly to Face's constructor, where they are used to initialize the corresponding instance variables.

The only method (other than the constructor) defined by SurprisedFace is the drawMouth method.  That is because the only behavioral different between a plain Face and a SurprisedFace is the mouth.  So, SurprisedFace overrides the definition of drawMouth, meaning that when the draw method is invoked on a SurprisedFace, it will use the overriden drawMouth method, even though all of the other drawing methods (draw, drawCircle, and drawEyes) are inherited from Face and not overridden.

Note that in order to get the values of the x coordinate, y coordinate, and size, the drawMouth method uses accessor methods (getX, getY, and getSize.)  It uses these values to draw an oval for the mouth rather than the horizontal line that the superclass Face draws.

The other two classes, SmileyFace [source code] and FrowneyFace [source code] are defined in much the same way, overriding the drawMouth method as appropriate.

The FaceDemo class [source code] defines a simple GUI application that demonstrates each Face class.  Two parts of this class are interesting.  First, references to the various Face objects are kept in instance variables, and the actual objects are created in the constructor:

public class FaceDemo extends JPanel {
  private Face face;
  private Face smileyFace;
  private Face frowneyFace;
  private Face surprisedFace;
  
  public FaceDemo() {
    face = new Face(60, 70, 100, Color.RED);
    smileyFace = new SmileyFace(180, 70, 100, Color.MAGENTA);
    frowneyFace = new FrowneyFace(300, 70, 100, Color.GREEN);
    surprisedFace = new SurprisedFace(420, 70, 100,
                                      Color.BLUE);
  }

Note how each instance variable is declared with the superclass Face as its type, even though each variable will actually be storing a different type of object.  This is an example of polymorphism, showing that an instance of any subclass of Face can be stored in a variable whose type is Face.

The second interesting detail is how the face objects are drawn in the window within the paint method:

  public void paint(Graphics graphics) {
    super.paint(graphics);
    face.draw(graphics);
    smileyFace.draw(graphics);
    frowneyFace.draw(graphics);
    surprisedFace.draw(graphics);
  }

Each object is drawn using the draw method defined in the superclass Face.  Each call to draw results in a call to drawMouth, but because the drawMouth method is overridden in three of the classes, the precise version of drawMouth that is called is determined by the runtime type of the object.  In other words, if the object is a SurprisedFace, then SurprisedFace's drawMouth method is called, and so forth.  This phenomenon---the type of the object determining which variant of a method is called---is known as dynamic dispatch, and is an extremely powerful programming technique.

When FaceDemo's main method is run, it creates a window and shows each kind of face: