INHERITANCE AND INTERFACES


Contents


INHERITANCE

Casting

Recall that inheritance implements the "is-a" relationship. For example, in the Introduction to Inheritance notes, class RaceHorse was defined to be a subclass of Horse (because every RaceHorse is a Horse). Therefore, it makes sense that a RaceHorse can be used in any context that expects a Horse. For example:

Variable h is of type Horse, so in an assignment of the form the right-hand side of the assignment should be of type Horse, too. However, since a RaceHorse is-a Horse, it is OK for the right-hand side to be of type RaceHorse, as in the above example.

Note that every Horse is not a RaceHorse, so in general, a Horse cannot be used when a RaceHorse is expected. For example, the following code causes a compile-time error:

Here are three more examples of code that sets a Horse variable or parameter to point to a RaceHorse object: If you know that at a particular point in your code a Horse variable is really pointing to a RaceHorse object, then you can use that variable in a context that expects a RaceHorse, but you must provide a cast. Note that there are two kinds of errors that can arise if you get this wrong:
  1. missing cast => compile-time error
  2. incorrect cast => runtime error (ClassCastException is thrown)

Examples:

Assume that we have the following declarations of function f and variables h1 and h2:

Now consider the following three calls to f:

  1. f(h1); // compile-time error (missing cast)
  2. f( (RaceHorse)h1 ); // fine! h1 really does point to a RaceHorse
  3. f( (RaceHorse)h2 ); // runtime error (bad cast) h2 points to a Horse

Note that when you use a cast you must think about what expression you are casting, and perhaps use parentheses (if that expression is part of a larger expression). For example, suppose variable h is of type Horse but actually points to a RaceHorse. You can call h's WinRace method (which is a RaceHorse method, but not a Horse method), but you have to use a cast, like this:

The parentheses tell the compiler that you are casting just variable h to be of type RaceHorse. If you omit the parentheses: the compiler will think you are casting the result of the call h.WinRace() to be of type RaceHorse.
TEST YOURSELF #1

Assume that classes List, Horse, and RaceHorse have been defined as usual, and that the following statements have been executed:

Question 1: Fill in the following table by specifying, for each of the assignments:
Assignment Type of lhsType of rhs Error?
h = L.nextElement();
h = (Horse)L.nextElement();
RaceHorse r = L.nextElement();
RaceHorse r = (RaceHorse)L.nextElement();

Question 2: For each of the following method invocations, say whether the code causes a compile-time error, a run-time error, or no error.

solution


Dynamic Dispatch

Suppose that both the Horse and RaceHorse classes define the method

i.e., the RaceHorse class overrides the Horse class's Train method. Given variable h of type Horse (but remember, it might actually point to a RaceHorse object) which version of the method is called when the code is executed? The answer is that it depends on what h actually points to (it does not depend on the declared type of h). If h points to a Horse object, then the Horse version of the method is called; if it points to a RaceHorse object, then the RaceHorse version is called. Note that this code might be executed many times, and that sometimes variable h might be pointing to a Horse, while at other times it might be pointing to a RaceHorse. So each time the code is executed it must be determined again which version of the Train method to call.

This approach to determining which version of an overridden method to call based on what the variable actually points to at the time of the call (rather than on the type of the variable) is called dynamic dispatch (or sometimes, dynamic binding). In C++, you get dynamic dispatch only if:

  1. the method call is via a pointer; e.g.: rather than via the object itself; e.g.: (except that if the variable is a reference parameter, then it is really a pointer, so the call h.Train() could use dynamic dispatch in that case)

  2. the method being called was declared virtual

Note that in Java all non-primitive types are pointers, so point (1) above does not apply (all method calls are via pointers), and that all non-static methods are (implicitly) virtual, so point (2) does not apply, either. (But it is important to note that static methods do not have dynamic dispatch. So if the Horse and RaceHorse classes both have a static method named foo, then if variable h is of type Horse, h.foo() will always call the Horse version, even if h is pointing to a RaceHorse object. This is one reason why static methods should really be called by using the class name rather than a variable name; e.g., Horse.foo() or RaceHorse.foo() -- it makes it clear which method is being called, and you won't be fooled into thinking that dynamic dispatch applies in this case.)

Also note that dynamic dispatch applies only to methods, not fields. For example, if Horse and RaceHorse both have a field named myValue (i.e., the RaceHorse class hides the myValue field defined by the Horse class), then if variable h is of type Horse, the expression h.myValue always refers to the field defined in the Horse class, even if h actually points to a RaceHorse object.


TEST YOURSELF #2

Consider the following program:

What is printed when this program is executed?

solution


Using Super

It is possible for a subclass to "request" its superclass's version of an overridden method by using "super.xxx", where xxx is the name of the method. For example, assume as above that both Horse and RaceHorse define the Train method. Normally, a call to Train from inside a RaceHorse method would call the RaceHorse version; however, the Horse version can be called by using super.Train. For example, the RaceHorse class could include a Nurture method, defined as follows (assuming that the Horse class has provided a GetAge method to access the myAge field):

Abstract Classes and Methods

Idea: An abstract method is a method that is declared in a class, but not defined. In order to be instantiated, a subclass must provide the definition. An abstract class is any class that includes an abstract method.

Motivation: Suppose you want to define a class hierarchy in which some method needs to be provided by all subclasses, but there is no reasonable default version (i.e., it is not possible to define a version of the method in the base class that makes sense for the subclasses). For example, assume that you want to use Java classes to define a data structure that will be used to represent flowcharts -- graphs in which nodes represent statements or conditions, and edges represent flow of control. Here's an example flowchart for a program that sums the numbers from 1 to N:

You might decide to use inheritance to define the different kinds of nodes, since different kinds of nodes need to store different information; for example:
  • condition node: store the boolean expression and two outgoing edges
  • read node: store the variable being read and one outgoing edge
  • assign node: store the variable being assigned to, the expression being assigned, and one outgoing edge Here's a reasonable class hierarchy for flowchart nodes: It will be useful to have a Print method for flowcharts, which means that every node should have a Print method. However, there is no reasonable Print method that will work for all nodes. Therefore, this is a time to use abstract methods! Here's the syntax:

    Notes:

    1. If a class includes an abstract method, the class must be declared abstract, too (otherwise you get a compile-time error).

    2. An abstract class cannot be instantiated:
          Node n;   // OK -- just a pointer to a Node, no attempt to create a Node object
          n = new Condition();  // OK -- Condition is not an abstract class
          n = new Node();       // Error!  Can't instantiate an abstract class
          

    3. A subclass of an abstract class that does not provide bodies for all abstract methods must also be declared abstract:
               class Statement extends Node { // Error!  Statement does not
               }                              // implement Print, so it must
                                              // be an abstract class
               

    4. A subclass of a non-abstract class can override a (non-abstract)method of its superclass, and declare it abstract. In that case, the subclass must be declared abstract.

    Inheritance Summary

    INTERFACES

    Motivation: Some objects have more than one "is-a" relationship. For example, consider designing classes to represent the people associated with a university:

    Note that a TA is certainly a student, but in some ways, a TA is also staff (e.g., a TA gets paid, receives benefits, etc). Some languages (e.g., C++) allow multiple inheritance; for example, the TA class could be a subclass of both the Student class and the Staff class. However, there are problems with multiple inheritance, and so Java forbids it, providing interfaces instead.

    An interface is similar to a class, but can only contain:

    Here's an example:

    Note that both methods are implicitly public and abstract (those keywords can be provided, but are not necessary).

    A class can implement one or more interfaces (in addition to extending one class). It must provide bodies for all of the methods declared in the interface, or else it must be abstract. For example:

  • Public interfaces (like public classes) must be in a file with the same name.

  • Many classes can implement the same interface (e.g., both the TA class and the Staff class can implement the Employee interface).

  • An interface can be a "marker": For example:
        interface GoodClass { }
        class C1 implements GoodClass { ... }
        ...
        C1 x = new C1();
        if (x instanceof GoodClass) ...  // this condition evaluates to true
        
    Cloneable is a useful marker interface, defined in java.lang (see Cloning above).

  • Interfaces provide a way to group similar objects. For example, you could write a function with a parameter of type Employee, and then call that function with either a TA or a Staff object. If you hadn't used the Employee interface (e.g., if you simply wrote RaiseSalary and GetSalary methods for the TA and Staff classes), writing such a function would be very clumsy.
    TEST YOURSELF #3

    Consider the following program:

    True or false:

    1. An attempt to compile this program fails because class Test tries to implement two interfaces at once ______.
    2. An attempt to compile this program fails because class Test only implements interfaces, it does not extend any class ______.
    3. An attempt to compile this program fails because the definitions of the two interfaces and the class are all in the same file ______.
    4. It is optional for class Test to implement the methods RaiseSalary and Play ______.

    solution


    Solutions to Self-Study Questions

    Test Yourself #1

    Test Yourself #2

    Test Yourself #3