Skip to main content

Section 8.7 Inheritance

We have seen how Java supports the programmer for encapsulating classes with visibility modifiers. Our example implementation of Vec2 now is encapsulated, but it is not yet separated from its specification: If a program declares a variable of type Vec2, it invariably refers to our implementation of Vec2, most recently using polar coordinates. In larger programs, we do not want our code to refer to a single concrete implementation, but rather an abstract data type (ADT) that provides a certain interface. Code that uses such an abstract data type can then work with every implementation of the abstract data type. Several implementations can then be used interchangeably, making the code more modular.

Subsection 8.7.1 Interfaces

Let us assume that our program uses Vec2s with cartesian coordinates as well as Vec2s with polar coordinates. Then, any reference to these classes in the program would need to concretely refer to one of the classes:

public class Rectangle {
  private PolarVec2 upperLeft, lowerRight;
  ...
}

We however do not want to distinguish between vectors in polar or cartesian coordinates when implementing the above class Rectangle. Any implementation of the ADT “two-dimensional vector” would do here. To achieve this flexibility, Java provides two important means: subtypes and interfaces. With an interface, we declare the signature of an abstract data type: All its method signatures with their parameter and return types.

public interface Vec2 {
  void setX(double x);
  void setY(double x);
  double getX();
  double getY();
  void scale(double r);
  void add(Vec2 v);
  double length();
}

All declared methods of the interface have public visibility. Note that interfaces cannot declare member fields. This is by design, because interfaces are intended to not be specific to the internal implementation of an ADT. Concrete implementations can now implement this (and an arbitrary number of other) interfaces.

public class PolarVec2 implements Vec2 {
  private double r, phi;

  public PolarVec2(double r, double phi) {
    this.r   = r;
    this.phi = phi;
  }

  public double getX() { return r * Math.cos(phi); }
  ...
  public void scale(double r) {
    setX(r * getX());
    setY(r * getY());
  }
  ...
}

public class CartesianVec2 implements Vec2 {
  private double x, y;

  public CartesianVec2(double x, double y) {
    this.x = x;
    this.y = y;
  }

  public double getX() { return x; }
  ...
  public void scale(double r) {
    setX(r * getX());
    setY(r * getY());
  }
  ...
}

PolarVec2 and CartesianVec2 are now subtypes of the type Vec2. We write this as PolarVec2 \(\subtyperel\) Vec2 and CartesianVec2 \(\subtyperel\) Vec2. If A is a subtype of B (we can also say: if A is a subclass of B), references to objects of the class A can also be put into the containers of variables of type B. In our example, this allows us to implement the class Rectangle against the interface Vec2:

public class Rectangle {
  private Vec2 upperLeft, lowerRight;

  public Rectangle(Vec2 upperLeft, Vec2 lowerRight) {
    this.upperLeft  = upperLeft;
    this.lowerRight = lowerRight;
  }
  ...
}

We however cannot create concrete Vec2 objects since the interface Vec2 does not provide a concrete implementation of its methods (as intended). We can use objects of arbitrary subclasses of Vec2 to initialize a Rectangle object:

public class RectTest {
  public static void main(String[] args) {
    Vec2 v = new PolarVec2(1, Math.PI); // possible since Polarvec
                                        //   is a subclass of Vec2
    Vec2 w = new CartesianVec2(1, 0);   // also possible
    Rectangle r = new Rectangle(v, w);
   ...
  }
}

The subtype relation is a partial order: It is

  • reflexive: For all types \(T: T \subtyperel T\)

  • transitive: For all types \(U, V, W: U \subtyperel V \land V \subtyperel W \implies U \subtyperel W\)

  • antisymmetric: For all types \(V, W: V \subtyperel W \land W \subtyperel V \implies V = W\)

Subsection 8.7.2 Overriding Methods

If we inspect the classes CartesianVec2 and PolarVec2 closer, we see that they have some code in common. In our reduced example, this is the case for the method scale. In a complete implementation, many more methods would be affected. scale only uses the setters and getters of Vec2 and no more implementation details of PolarVec2 or CartesianVec2. The current implementation duplicates the code of scale. Code duplication is the enemy of every programmer and should be avoided in all cases: If duplicated code is erroneous, it needs to be fixed at multiple source locations that might not be easy to find. Java helps here as well by allowing us to insert a class between the interface Vec2 and the concrete implementations CartesianVec2 and PolarVec2, implementing parts of Vec2 but leaving others abstract.

public abstract class BaseVec2 implements Vec2 {
  public void scale(double r) {
    setX(r * getX());
    setY(r * getY());
  }
  public double length() {
    double x = getX();
    double y = getY();
    return Math.sqrt(x * x + y * y);
  }
}

public class PolarVec2 extends BaseVec2 {
  ...
}

public class CartesianVec2 extends BaseVec2 {
  ...
}

Just as for Vec2, we cannot create concrete BaseVec2 objects since it does not provide implementations for all methods of the interface. Java forces us to mark the unfinished state of BaseVec2 by using the abstract keyword in the class declaration. We cannot create objects of abstract classes. PolarVec2 and CartesianVec2 then inherit from BaseVec2 and override the remaining methods of Vec2.

Remark 8.7.1.

It is noteworthy that interfaces are only a special kind of class. All vocabulary for classes also applies to interfaces. That Java requires implements for interfaces and extends for classes is not a conceptional, but rather an aesthetic difference.

When inheriting from a class, we can also override methods that already have an implementation in the base class. In this case, the subclass discards the implementation of the overridden method of the base class and uses its own. For example, PolarVec2 can compute the vector's length more efficiently than it is done in BaseVec2:

public PolarVec2 extends BaseVec2 {
  private double r, phi;

  public double length() { return r; }
}

These constructs constitute an inheritance hierarchy (Figure 8.7.2). In practice, such hierarchies can easily span several dozen classes. Inheritance hierarchies typically tend to be shallow and broad rather than deep.

Figure 8.7.2. Inheritance hierarchy of Vec2

Subsection 8.7.3 Calling a Method

The type of a Variable in Java (if its type is a class), is only an upper bound to the concrete type of the object that it references. The variable declaration T a; only states that the container for a contains a reference to an object whose type is at least T. Objects of any subclass of T are allowed as well. This property of Java's type system enables the easy replacement of implementations (subclasses). This however means that there can be an ambiguity when calling a method. Consider the above example: The method length is implemented by BaseVec2 and by PolarVec2. Which method is actually called depends on the concrete type of the object. The concrete type of an object is that whose constructor was used to initialize the object.

Vec2 v;
double l;
v = new PolarVec2(1, 0.5);
// calls length from PolarVec2
l = v.length();

v = new CartesianVec2(1, 0);
// calls length from CartesianVec2
l = v.length();

The concrete type of the argument v therefore decides which method needs to be called. This is another reason for the special syntax of the this parameter.

It is important that we understand the difference between the static type and the concrete type. The static type is the type that the compiler can determine at compile time of a variable. It is derived directly from the variable's declaration and is, as mentioned previously, an upper bound for the concrete type:

\begin{equation*} \text{concrete type} \subtyperel \text{static type} \end{equation*}

In the above example, the static type of the variable v is Vec2, at every method call. The called method is however chosen by the concrete type.

Remark 8.7.3.

It is not possible, in general, for the compiler to derive the concrete type of an object at a certain place in the program:

if ( /* today is Tuesday */ )
  v = new PolarVec2(1, 0.5);
else
  v = new CartesianVec2(1, 0);
double l = v.length();

The run time environment therefore needs to perform additional tasks to keep track of the concrete types of objects when the program is executed.

Remark 8.7.4.

With the instanceof operator, we can test at run time whether the concrete type of an object is a subclass of a given class. This is a common tool to regain static type information:

if (v instanceof PolarVec2) {
  PolarVec2 w = (PolarVec2)v;
}

The type conversion (PolarVec2)v checks at run time whether the concrete type of the object refered to by the container of v is a subtype of PolarVec2. If that is not the case, an exception is triggered when the program is executed.

Subsection 8.7.4 The Class Object

Every class in Java inherits from the class Object, directly or indirectly. If no base class is provided at a class declaration, Java assumes an implicit extends Object. The class Object provides several methods that are important for the interplay with the Java standard library. It might be necessary to override them with suitable custom implementations. We will discuss the more important ones in the following subsections.

Subsection 8.7.5 equals

Java distinguishes objects that are equal from those that are identical:

Vec2 v = new PolarVec2(1, 0.5);
Vec2 w = new PolarVec2(1, 0.5);

The objects v and w should be equal, but they are not identical. They are two separate objects that happen to have the same values in their fields. The equality operator == in Java compares only references, therefore

System.out.println(v == w);

prints false.

Often, the equality of objects is of more interest than their identity. Especially when working with immutable classes, it is common to have several objects that are equal at the same time. Java therefore uses equality rather than identity at many places, including the collections framework, which provides implementations of common data structures. This requires a class to define when two of its objects are equal. It needs to override the method boolean equals(Object) of the class Object:

public class BaseVec2 {
  ...
  public boolean equals(Object o) {
    // Stop if o has not at least the type Vec2.
    // This includes the case that o == null.
    if (!(o instanceof Vec2))
      return false;
    // o has at least type Vec2 and is not null
    Vec2 v = (Vec2)o;
    // the vectors are equal if their coordinates are equal:
    return getX() == v.getX() && getY() == v.getY();
  }
}

Remark 8.7.5.

In the above example, it is essential to use Object as the parameter type of equals. It is a common mistake to use the current class as the parameter type for the object to compare with. In our example, this would be boolean equals(BaseVec2). Java will not note an error in this case, since this overloads the method equals rather than overriding it (Section 8.8).

Remark 8.7.6.

The default implementation of equals in Object is

public boolean equals(Object o) {
  return this == o;
}

Subsection 8.7.6 hashCode

The method hashCode serves to compute for an object an integer that is as unique as possible. We need this functionality to place an object in a hash table. It is only necessary to override hashCode if equals has also been overridden. The reason for this is that hash tables require that two equal objects always have the same hashCode. That is, if, for two objects p and q, p.equals(q) holds, hashCode needs to be implemented such that p.hashCode() == q.hashCode().

The Java compiler cannot enforce this condition statically. It is up to the programmer to obey it. The collections framework in Java's standard library uses the hashCode method to implement hash maps and sets as discussed in Section 5.3.

Subsection 8.7.7 toString

This method is used to provide a “human-readable” textual representation of an object. Java calls toString at several places to obtain this textual representation:

  • Many output methods, for instance PrintStream.println(), accept a reference of type Object and then use toString to get a textual representation to print.

  • The operator + is overloaded with several meanings in Java. If one of its operands has type String and the other is at least an Object, this operand is converted to a textual representation via toString, which is then concatenated to the String object.

A reasonable implementation for our example is the following:

public class BaseVec2 {
  public String toString() {
    return "[" + getX() + ", " + getY() + "]";
  }
}