Section 4.2
Inheritance and Polymorphism; "this" and "super"


A CLASS REPRESENTS A SET OF OBJECTS which share the same structure and behaviors. The class determines the structure of objects by specifying variables that are contained in each instance of the class, and it determines behavior of objects by providing the instance methods that express the behavior of the objects. This is a powerful idea. However, something like this can be done in most programming languages. The central new idea in object-oriented programming -- the idea that really distinguishes it from traditional programming -- is to allow classes to express the similarities among objects that share some, but not all, of their structure and behavior. Such similarities can expressed using inheritance.

The term "inheritance" refers to the fact that one class can inherit part or all of its structure and behavior from another class. The class that does the inheriting is said to be a subclass of the class from which it inherits. If class B is a subclass of class A, we also say that class A is a superclass of class B. (Sometimes the terms derived class and base class are used instead of subclass and superclass.) A subclass can add to the structure and behavior that it inherits. It can also replace or modify inherited behavior (though not inherited structure). The relationship between subclass and superclass is sometimes shown by a diagram in which the subclass is shown below, and connected to, its superclass.

In Java, when you create a new class, you can declare that it is a subclass of an existing class. If you are defining a class named "B" and you want it to be a subclass of a class named "A", you would write

        class B extends A {
            .
            .  // additions to, and modifications of,
            .  // stuff inherited from class A
            .
        }

Several classes can be declared as subclasses of the same superclass. The subclasses, which might be referred to as "sibling classes," share some structures and behaviors -- namely, the ones they inherit from their common superclass. The superclass expresses these shared structures and behaviors. In the diagram to the left, classes B, C, and D are sibling classes. Inheritance can also extend over several "generations" of classes. This is shown in the diagram, where class E is a subclass of class D which is itself a subclass of class A. In this case, class E is considered to be a subclass of class A, even though it is not a direct subclass.



Let's look at an example. Suppose that a program has to deal with motor vehicles, including cars, trucks, and motorcycles. (This might be a program used by a Department of Motor Vehicles to keep track of registrations.) The program could use a class named Vehicle to represent all types of vehicles. The Vehicle class could include instance variables such as registrationNumber and owner and instance methods such as transferOwnership(). These are variables and methods common to all vehicles. Three subclasses of Vehicle -- Car, Truck, and Motorcycle -- could then be used to hold variables and methods specific to particular types of vehicles. The Car class might add an instance variable numberOfDoors, the Truck class might have numberOfAxels, and the Motorcycle class could have a boolean variable hasSidecar. (Well, it could in theory at least, even if it might give a chuckle to the people at the Department of Motor Vehicles.) The declarations of these classes in Java program would look, in outline, like this:

     class Vehicle {
        int registrationNumber;
        Person owner;  // (assuming that a Person class has been defined)
        void transferOwnership(Person newOwner) {
            . . .
        }
        . . .
     }
     class Car extends Vehicle {
        int numberOfDoors;
        . . .
     }
     class Truck extends Vehicle {
        int numberOfAxels;
        . . .
     }
     class Motorcycle extends Vehicle {
        boolean hasSidecar;
        . . .
     }

Suppose that myCar is a variable of type Car that has been declared and initialized with the statement

Car myCar = new Car();

(Note that, as with any variable, it is OK to declare a variable and initialize it in a single statement. This is equivalent to the declaration "Car myCar;" followed by the assignment statement "myCar = new Car();".) Given this declaration, a program could to refer to myCar.numberOfDoors, since numberOfDoors is an instance variable in the class Car. But since class Car extends class Vehicle, a car also has all the structure and behavior of a vehicle. This means that myCar.registrationNumber, myCar.owner, and myCar.transferOwnership() also exist.

Now, in the real world, cars, trucks, and motorcycles are in fact vehicles. The same is true in a program. That is, an object of type Car or Truck or Motorcycle is automatically an object of type Vehicle. This brings us to the following Important Fact:

A variable that can hold a reference
to an object of class A can also hold a reference
to an object belonging to any subclass of A.

The practical effect of this in our example is that an object of type Car can be assigned to a variable of type Vehicle. That is, it would be legal to say

Vehicle myVehicle = myCar;

or even

Vehicle myVehicle = new Car();

After either of these statements, the variable myVehicle holds a reference to a Vehicle object that happens to be an instance of the subclass, Car. The object "remembers" that it is in fact a Car, and not just a Vehicle. Information about the actual class of an object is stored as part of that object. It is even possible to test whether a given object belongs to a given class, using the instanceof operator. The test:

if (myVehicle instanceof Car) ...

determines whether the object referred to by myVehicle is in fact a car.

On the other hand, if myVehicle is a variable of type Vehicle the assignment statement

myCar = myVehicle;

would be illegal because myVehicle could potentially refer to other types of vehicles besides cars. This is similar to a problem we saw previously in Section 2.7: The computer will not allow you to assign an int value to a variable of type short, because not every int is a short. Similarly, it will not allow you to assign a value of type Vehicle to a variable of type Car because not every vehicle is a car. As in the case of ints and shorts, the solution here is to use type casting. If, for some reason, you happen to know that myVehicle does in fact refer to a Car, you can use the type cast (Car)myVehicle to tell the computer to treat myVehicle as if it were actually of type Car. So, you could say

myCar = (Car)myVehicle;

and you could even refer to ((Car)myVehicle).numberOfDoors. As an example of how this could be used in a program, suppose that you want to print out relevant data about a vehicle. You could say:

           System.out.println("Vehicle Data:");
           System.out.println("Registration number:  " + myVehicle.registrationNumber);
           if (myVehicle instanceof Car) {
              System.out.println("Type of vehicle:  Car");
              Car c = (Car)myVehicle;
              System.out.println("Number of doors:  " + c.numberOfDoors);
           }
           else if (myVehicle instanceof Truck) {
              System.out.println("Type of vehicle:  Truck");
              Truck t = (Truck)myVehicle;
              System.out.println("Number of axels:  " + t.numberOfAxels);
           }
           else if (myVehicle instanceof Motorcycle) {
              System.out.println("Type of vehicle:  Motorcycle");
              Motorcycle m = (Motorcycle)myVehicle;
              System.out.println("Has a sidecar:    " + m.hasSidecar);
           }

Note that for object types, when the computer executes a program, it checks whether type casts are valid. So, for example, if myVehicle refers to an object of type Truck, then the type cast (Car)myVehicle will produce an error.


As another example, consider a program that deals with shapes drawn on the screen. Let's say that the shapes include rectangles, ovals, and roundrects of various colors.

(illustration showing various shapes)

Three classes, Rectangle, Oval, and RoundRect, could be used to represent the three types of shapes. These three classes could have a common superclass, Shape, to represent features that all three shapes have in common. The Shape class could include instance variables to represent the color, position, and size of a shape. It could include instance methods for changing the color, position, and size of a shape. Changing the color, for example, might involve changing the value of an instance variable, and then redrawing the shape in its new color:

       class Shape {
       
           Color color;   // color of shape.  Note that class Color
                          // is defined in package java.awt.  Assume
                          // that this class has been imported.
                          
           void setColor(Color newColor) {
                 // method to change the color of the shape
              color = newColor; // change value of instance variable
              redraw(); // redraw shape, which will appear in new color
           }
           
           void redraw() {
                 // method for drawing the shape
              ? ? ?  // what commands should go here?
           }

           . . .          // more instance variables and methods

       } // end of class Shape

Now, you might see a problem here with the method redraw(). The problem is that each different type of shape is drawn differently. The method setColor() can be called for any type of shape. How does the computer know which shape to draw when it executes the redraw()? Informally, we can answer the question like this: The computer executes redraw() by asking the shape to redraw itself. Every shape object knows what it has to do to redraw itself.

In practice, this means that each of the specific shape classes has its own redraw() method:

       class Rectangle extends Shape {
          void redraw() {
             . . .  // commands for drawing a rectangle
          }
          . . . // possibly, more methods and variables
       }
       class Oval extends Shape {
          void redraw() {
             . . .  // commands for drawing an oval
          }
          . . . // possibly, more methods and variables
       }
       class RoundRect extends Shape {
          void redraw() {
             . . .  // commands for drawing a rounded rectangle
          }
          . . . // possibly, more methods and variables
       }

If oneShape is a variable of type Shape, it could refer to an object of any of the types Rectangle, Oval, or RoundRect. As a program executes, and the value of oneShape changes, it could even refer to objects of different types at different times! Whenever the statement

oneShape.redraw();

is executed, the redraw method that is actually called is the one appropriate for the type of object to which oneShape actually refers. There may be no way of telling, from looking at the text of the program, what shape this statement will draw, since it depends on the value that oneShape happens to have when the program is executed. Even more is true. Suppose the statement is in a loop and gets executed many times. If the value of oneShape changes as the loop is executed, it is possible that the very same statement "oneShape.redraw();" will call different methods and draw different shapes as it is executed over and over. We say that the redraw() method is polymorphic. A method is polymorphic if the action performed by the method depends on the actual type of the object to which the method is applied. Polymorphism is one of the major distinguishing features of object-oriented programming.

Perhaps this becomes more understandable if we change our terminology a bit: In object-oriented programming, calling a method is often referred to as sending a message to an object. The object responds to the message by executing the appropriate method. The statement "oneShape.redraw();" is a message to the object referred to by oneShape. Since that object knows what type of object it is, it knows how it should respond to the message. From this point of view, the computer always executes "oneShape.redraw();" in the same way: by sending a message. The response to the message depends, naturally, on who receives it. From this point of view, objects are active entities that send and receive messages, and polymorphism is a natural, even necessary, part of this view. Polymorphism just means that different objects can respond to the same message in different ways.

One of the most beautiful things about polymorphism is that it let's code that you write do things that you didn't even conceive of, at the time you wrote it. If for some reason, I decide that I want to add beveled rectangles to the types of shapes my program can deal with, I can write a new subclass, BeveledRect, of class Shape and give it is own redraw() method. Automatically, code that I wrote previously -- such as the statement oneShape.redraw() -- can now suddenly start drawing beveled rectangles, even though the beveled rectangle class didn't exist when I wrote the statement!



"this" and "super"

In the statement "oneShape.redraw();", the redraw message is sent to the object oneShape. Look back at the method from the Shape class for changing the color of a shape:

           void setColor(Color newColor) {
              color = newColor; // change value of instance variable
              redraw(); // redraw shape, which will appear in new color
           }

A redraw message is sent here, but which object is it sent to? Well, the setColor method is itself a message that was sent to some object. The answer is that the redraw message is sent to that same object, the one that received the setColor message. If that object is a rectangle, then it is the redraw() method from the Rectangle class is executed. If the object is an oval, then it is the redraw() method from the Oval class. This is what you should expect, but it means that the redraw(); statement in the setColor() method does not necessarily call the redraw() method in the Shape class. The redraw() method that is executed could be in any subclass of Shape.

Although you don't usually have to specify it by name, sometimes it is useful to have a name for "the object that received this message." Java gives you not one but two names for this object, depending on what you want to do with it. Whenever an instance method is executed, the system sets up two special variables to refer to the object that received the message. The variables are named this and super. You can use these variables in any instance method.

The variable "this" refers to this object, the one that received this message. You might need to use this if you want to assign the object to a variable or pass it as a parameter. This is quite common, actually. Consider, for example, the Shape class, which represents shapes on a computer screen. Suppose that shapes can be "selected," and you want to keep track of which shape is currently selected. A static variable, selectedShape, could be added to the class to keep track of which shape is currently selected. (Make sure that you understand why it is static!) An instance method, select(), that is used to select a shape would then assign this to selectedShape:

      class Shape {
         static Shape selectedShape = null;  // currently selected shape
         void select() {  // instance method for selecting this shape
            selectedShape = this;  // record that this is the selected shape
         }
         . . .  // other variables and methods
      }

Another use of "this" is to clear up ambiguities when there are two variables or parameters or local variables of the same name. For example, consider this slight rewrite of the setColor() method from the shape class, in which I've renamed the parameter from newColor to color:

           void setColor(Color color) {
              this.color = color; // change value of instance variable
              redraw(); // redraw shape, which will appear in new color
           }

Inside the method setColor(), there are two things with the same name: A parameter and an instance variable are both named "color." The rule in situations like this is that the parameter hides the instance variable, so that when the name "color" is used by itself, it refers to the parameter. Fortunately, the instance variable can still be accessed by referring to it with the compound name "this.color", which can only mean an instance variable in the object, this. (You could still ask whether it's reasonable to use the same name for instance variable and parameter. It probably is; it saves you from having to make up some funny new name, such as newColor, for the parameter.)

The variable named super is even more important than the variable named this, but it is also more subtle: super refers to the same object as this, but it refers to that object treated as if it were a member of the superclass of the class containing the method that you are writing. So, for example, super.x would refer to an instance variable named x in the superclass of the current class. This can be useful for the following reason: If a class contains an instance variable with the same name as an instance variable in its superclass, then an object of that class will actually contain two variables with the same name: one defined as part of the class itself and one defined as part of the superclass. The variable in the subclass does not replace the variable of the same name in the superclass; it merely hides it. The variable from the superclass can still be accessed, using super. (Again, you might ask whether its reasonable to name variables in this way. The answer is, probably not.)

When you write a method in a subclass that has the same signature as a method in its superclass, the method from the superclass is hidden in the same way. We say that the method in the subclass overrides the method from the superclass. Again, however, super can be used to access the method from the superclass.

The major use of super is to override a method with a new method that extends the behavior of the inherited method, instead of replacing that behavior entirely. The new method can use super to call the inherited method, and it can include additional code to provide additional behavior. As a somewhat silly example,

       class FilledRectangle extends Rectangle {
          void redraw() {
             super.redraw();  // call redraw method from class Rectangle
             fill();   // then do some additional stuff
          }
          .
          .

As a more realistic example, suppose that the class TextBox represents a box on the screen where the user can type some input. Let's say that TextBox has an instance method called key which is called whenever the user presses a key on the keyboard. The purpose of this method is to add the character that the user has typed to the text box. Now, suppose I want a subclass, NumberBox, of TextBox that will let the user type in an integer. I only want to allow digits from 0 to 9 in the box, not letters or punctuation characters. I could define NumberBox like this:

        class NumberBox extends TextBox {
           void key(char ch) {
                 // user has typed the character ch; enter it in
                 // the box, but only if it is a digit in the
                 // range '0' to '9'; otherwise, ignore it
              if (ch >= '0' && ch <= '9')
                 super.key(ch);
           }
         }

[ Next Section | Previous Section | Chapter Index | Main Index ]