Inheritance and the specialization interface

Recall that we identified three key elements of OO programming:

We now discuss the third of these, inheritance. Inheritance is a mechanism that supports extensibility and reuse of code, both across programs and within programs.

Motivating example

Suppose someone gives us an implementation of the Puzzle interface that we saw in the previous lecture. In the APuzzle implementation, the puzzle is represented by a 2D array of integers:

class APuzzle implements Puzzle {
   private int[][] tiles;

   public int tile(int r, int c) {
      return tiles[r][c];
   }
   public void move(Direction d) { ... }
   ...
}

Now suppose that we want to build a better puzzle implementation by reusing this code. For example, we might want the puzzle to log all of the moves that have been made. We could proceed by copying the code of APuzzle to create a new class LogPuzzle, which we could then add new fields and methods to. But this strategy doesn't work very well when the original supplier of APuzzle fixes some bugs and gives us a new version APuzzle'. Now we have to either apply the same bug fixes to LogPuzzle on the code that we copied from APuzzle to get a new LogPuzzle', or start afresh from APuzzle' and build a new LogPuzzle' from it. Either way, it will be a lot of work and difficult to automate.

Figure 1: The problem of merging upgrades

At least for some kinds of changes that we might make between APuzzle and LogPuzzle, inheritance offers a solution to this problem. We can think of inheritance as a language mechanism for copying a class and making certain kinds of changes to that class, without actually physically copying the code.

To add the new functionality to APuzzle, we build a new class LogPuzzle that inherits all the functionality from APuzzle. This is done by declaring that LogPuzzle extends APuzzle:

class LogPuzzle extends APuzzle {
   private int numMoves;

   public int numMoves() {
      return numMoves;
   }

   public void move(Direction d) {
      numMoves++;
      super.move(d);
   }
}

Here APuzzle is the superclass and LogPuzzle is the subclass. In general, classes inheriting from other classes form an inheritance hierarchy or class hierarchy, in which superclasses are above their subclasses.

The LogPuzzle class is just like the APuzzle class, except:

Method dispatch and inheritance

Because of the subtyping relationship between the two classes, we can write the following code:

APuzzle p = new LogPuzzle();
p.move(up); // which move?

Now here is an interesting question. There are move methods in both LogPuzzle and APuzzle, and they do different things. Which one is executed in the call p.move(up)? The static type of p is APuzzle, but at the time of the call, p refers to an object of dynamic type LogPuzzle.

In Java, instance (non-static) methods such as move are dispatched according to the dynamic type of the receiver object (the class from which it was originally created via new). So it is the LogPuzzle version that is run. This is known as dynamic dispatch.

As a slightly more complicated example, suppose the superclass APuzzle has a method scramble that calls move, but LogPuzzle does not have a scramble method:

class APuzzle {
   public void scramble() {
      ... move(randomDir) ...
   }
}

APuzzle p = new LogPuzzle();
p.scramble();
// is p.numMoves() equal to 0?

When p.scramble() is called, the scramble method of APuzzle is invoked. But when that method calls move, which version of move is dispatched? It is the move method of LogPuzzle. The move method is still an instance method, and as above, the method dispatch mechanism starts at the dynamic class of the receiver object and searches upward until finding an implementation. In this case it finds one immediately in the LogPuzzle class, and that is the one that is dispatched.

There are two different ways to understand how inheritance works when an instance method is invoked. One way is to say that the system searches upward through the inheritance hierarchy starting at the dynamic type of the object, looking for the first implementation of the method. But it is (almost) equivalent to say that the methods from the superclasses are copied down to their subclasses, except when overridden.

We say "almost" because there are some cases where code cannot be naively copied down verbatim. For example, a method in the superclass can refer to a instance variable in the superclass that is shadowed by a instance variable of the same name in a subclass. If the method in the superclass refers to this instance variable, then it still refers to the same instance variable even after it is copied down to the subclass. The identity of the instance variable being named is fixed at the time the superclass is compiled.

For example, consider these class definitions:

class A {
   int x = 3;
   void print() {
      System.out.println(x);
   }
}

class B extends A {
   int x = 4;
   void print() {
      System.out.println(x);
   }
}

If you write A a = new B(); a.print(), then 4 will be printed (dynamic dispatch!). However, if you got rid of the print method in B, say

class A {
   int x = 3;
   void print() {
      System.out.println(x);
   }
}

class B extends A {
   int x = 4;
}

and then said B b = new B(); b.print(), then 3 would be printed. When the inherited method refers to x, it still refers to the original, shadowed instance variable rather than the newly declared one.

Inheritance and static methods

Static methods complicate the story slightly, because they cannot be overridden by subclasses. Unlike instance methods, the choice of what version to call is based purely on static information available at compile time, before the program is run. The version that is invoked is the version of the class to which the method belongs, and does not change when the code making the call is inherited by a subclass. Consider the following code:

class A {
   static int f() { ... }
   void g() {
      f();
   }
}

class B extends A {
   static int f() { ... }
}

A x = new B();
x.g();

Classes A and B both have static methods f(). When the method g() is called, the call to f() inside g() invokes the A version of f rather than the B version. Because f is a static method, the call to f() is exactly the same as if it were written A.f(). It does not change in meaning when it is inherited by B.

The special syntax super.f() is also a static call that always invokes the parent class's version of f.

Constructors

Constructors are special static methods that are called with new to create and initialize new instances of the class. If the programmer does not specify a constructor, there is an implicit one with no arguments that just creates the object but doesn't do anything else. If the programmer specifies an explicit constructor, the implicit constructor with no arguments is no longer available. In that case, the constructors of a subclass must call a superclass constructor explicitly, as in the following example:

class APuzzle {
   private int[][] tiles;
   public APuzzle(int size) {
      tiles = new int[size][size];
   }
}

class LogPuzzle extends APuzzle {
   private int numMoves;
   public LogPuzzle() {
      super(4);
      numMoves = 0;
   }
}

Here, the constructor LogPuzzle always creates puzzles of size 4×4, which is accomplished by calling the superclass constructor with super(4). The call to the superclass constructor is static.

Protected visibility

What if we want the LogPuzzle code to access the tiles field directly? As defined, we cannot, because tiles is declared private. However, if we give tiles the protected visibility in APuzzle, it becomes visible to subclasses:

class APuzzle {
   protected int[][] tiles;
   ...
}

Protected fields and methods form a second interface to a class. Public methods and fields are the public interface, which is exposed to client code. Protected methods and fields are the specialization interface, which is available to subclasses but not to ordinary clients. One of the challenges of good object-oriented design is to design both of these interfaces effectively, without confusing their roles. Designing a good specialization interface is especially important for object-oriented libraries where the classes provided by the library are intended to be extended through inheritance.

specialization interface

Figure 2: the specialization interface

Protected methods and the specialization interface

Suppose that scramble() had been defined to call a protected method internalMove instead of the public move method:

class APuzzle {
   public void scramble() {
      ... internalMove(n); ...
   }
   protected internalMove(int d) { ... }
}

class LogPuzzle extends APuzzle {
   private int numMoves = 0;
   protected internalMove(int d) {
      numMoves++;
      super.internalMove(d);
   }
}

This example shows that the specialization interface of APuzzle allows the LogPuzzle class to change the behavior of existing public methods without overriding them directly. Protected methods are hooks for future extensibility of OO code. The specialization interface defines how code can be extended.

Abstract classes

An abstract class is a class that provides some state and behavior that can be inherited by other classes, but that cannot be instantiated with new. Thus an abstract class cannot be the dynamic class of any object. An abstract class, indicated by the keyword abstract in the class declaration, may similarly declare methods that are marked abstract. Such methods do not need to come with an implementation, but any non-abstract subclass must implement them.

Abstract classes are useful as a way to factor out and centralize common functionality needed by a group of related classes. Using inheritance in this way is much better than copying the code and state into all the classes, because the code occurs only in one place.

One useful pattern is to use such methods as holes in the implementation to be filled in by subclasses. In this case, the protected visibility is appropriate because the methods are not intended to be used directly by clients.

Behavioral subtyping