Link Menu Search Expand Document

Inheritance

ed Lesson

Table of contents


We’ve often declared our data structures using the following syntax, prefering to use the interface type wherever possible.

List<Integer> numbers = new ArrayList<Integer>();

In this lesson, we’ll learn how this works in Java and why code sometimes fails to compile.

Terminology

Inheritance is a way to form an is-a relationship between two classes. The class declaration public class B extends A states that B is a sub-class of A (and that A is a super-class of B).

This relationship allows the programmer to use an object of type B anywhere the parameter or variable type A appears. For example, SoftwareEngineer could extend Engineer since a software engineer is a specialized version of an engineer: software engineers share many of the same behaviors as engineers in general, but they also have specialized behaviors as well.

By default, a sub-class inherits all of the fields and methods defined in its super-class. If a method is not defined in the sub-class, Java will fallback to the method found in the super-class. The sub-class is able to override methods (or add entirely new methods) to provide its own specialized behaviors.

A class can only extend one super-class. All classes, by default, extend the Object class. This is why all Java objects have a toString and equals method: they’re inherited from Object.toString and Object.equals.

Non-static method calls are polymorphic. This means that the method call is always called on the actual type of the object, so a call to method will always use the method in the actual type of the object (or the method inherited from the nearest super-class).

A sub-class can access the methods of its super-class. super.method() calls the super-class method with no arguments. super method calls are not polymorphic.

Mystery

Consider the following classes.

class Denny extends John {
    public void method1() {
        System.out.print("denny 1 ");
    }
    public String toString() {
        return "denny " + super.toString();
    }
}
class Cass {
    public void method1() {
        System.out.print("cass 1 ");
    }
    public void method2() {
        System.out.print("cass 2 ");
    }
    public String toString() {
        return "cass";
    }
}
class Michelle extends John {
    public void method1() {
        System.out.print("michelle 1 ");
    }
}
class John extends Cass {
    public void method2() {
        method1();
        System.out.print("john 2 ");
    }
    public String toString() {
        return "john";
    }
}

What is the output of the following code snippet?

Cass[] elements = {new Cass(), new Denny(), new John(), new Michelle()};
for (int i = 0; i < elements.length; i++) {
    elements[i].method1();
    System.out.println();

    elements[i].method2();
    System.out.println();

    System.out.println(elements[i]);

    System.out.println();
}

Solving inheritance mysteries

It’s important to develop a process for managing the information presented in this problem. Start by writing out the inheritance hierarchy.

  • Cass
    • John
      • Denny
      • Michelle

Then, build a method lookup table based on the hierarchy. If a class does not define a method, it gets a copy of its super class’ method.

 method1method2toString
Cassp("cass 1 ");p("cass 2 ");"cass"
Johnp("cass 1 ");method1(); p("john 2 ");"john"
Dennyp("denny 1 ");method1(); p("john 2 ");"denny john"
Michellep("michelle 1 ");method1(); p("john 2 ");"john"

Building up the table is normally the hard part, now all we have to do is use the table to answer the questions for the output. For each object in elements, give the result of calling method1, method2, and toString.

What is the output for the element at index 0 (Cass)?
cass 1
cass 2
cass
What is the output for the element at index 1 (Denny)?
denny 1
denny 1 john 2
denny john
What is the output for the element at index 2 (John)?
cass 1
cass 1 john 2
john
What is the output for the element at index 3 (Michelle)?
michelle 1
michelle 1 john 2
john

Music players case study

Before running a Java program, it needs to be compiled using the javac command. When Java compiles a piece of code, it will reference this hierarchy internally with the goal of checking to ensure that the code can run. We’ve probably seen dozens of times where the compiler catches mistakes like assigning values of the wrong type to a variable or calling a method that doesn’t exist.

Stack<Integer> s = new Stack<Integer>();
s.containsKey(5); // Stack cannot call containsKey

The compilation process is not as simple with inheritance. Let’s consider the following inheritance hierarchy representing real-world music players.

Music players

  • Object
    • MusicPlayer
      • MP3Player
        • iPod
          • iPhone
        • Zune
      • TapeDeck
      • CDPlayer

As we saw with interface types, the left-hand side type (promise type) does not need to be the same as the right-hand side type (actual type). Likewise, we can declare a MusicPlayer as the promise type with its value as a reference to the actual type iPhone, for example.

MusicPlayer p = new iPhone();

Java checks this line against the inheritance hierarchy.

Is iPhone a MusicPlayer?

Since iPhone is a MusicPlayer, we can call p.play() to play music.

However, we cannot call p.phoneCall() even though the actual type is iPhone. By declaring the promise type as MusicPlayer, the Java compiler can only be guaranteed access to the methods and fields of MusicPlayer and its super-classes. The intuition behind this is that someone might write a method that returns a MusicPlayer, but randomly picks between an MP3Player, a TapeDeck, or a CDPlayer as the actual type, so there’s no way for Java to know until running the code the actual type of p.

MusicPlayer p = new iPhone();
p.play();      // plays music
p.phoneCall(); // compilation error: MusicPlayer cannot call phoneCall

However, we know that p has an actual type of iPhone.

Casting

Use a cast to temporarily change the promised type of a value. This is typically used in inheritance to temporarily change the promised type from a more general type to a more specific type. For instance, (iPhone) p casts the value of a variable p (with actual type iPhone) to the promised type iPhone.

MusicPlayer p = new iPhone();
((iPhone) p).phoneCall();

The Java compiler performs a set of consistency checks at compile time to confirm that the cast is possible, and then another set of consistency checks at runtime when actually evaluating the line of code.

Compile-time check
Is it possible for the value of a variable p with promised type MusicPlayer to actually store a reference to an iPhone (or a subtype of iPhone)?
Runtime check
Check to make sure that the actual type works with the promised type, otherwise throw a ClassCastException.

The runtime check is necessary since it’s possible that the actual type of p is actually MP3Player, TapeDeck, or some other sub-class of MusicPlayer that isn’t an iPhone.

MusicPlayer p = new MP3Player();
((iPhone) p).phoneCall();

This code will compile since it’s possible that p has an actual type of iPhone based on the relationship between MusicPlayer and iPhone, but it will throw a ClassCastException at runtime.

Note
Casting does not change the actual type of an object, but rather only changes the promised type of the expression that evaluates to the object! It’s not possible to change the actual type of an object using casting!

Summary

Java code needs to be compiled before it can be executed. This process is fairly complicated!

Compilation
  1. Determine the promised type of the object in an expression. Validate any casting by only inspecting promised types and use the cast’s promised type if it is valid.
  2. Determine the compile-time method signature, which is the combination of method name and parameters. Start looking in the promised type’s declared class and then look in its super-classes. The chosen method signature must exactly match in name. If there are multiple methods in the class that could work with the promised types, choose the method signature that most closely matches the promised types.
Runtime
  1. If the expression involves casting, validate the cast by double checking that the actual type fits with the promised type. If the cast is invalid, throw a ClassCastException.
  2. If the method is a static method or if it is accessed through the super keyword, call the method determined during compilation. For all other methods (polymorphic methods), based on the method signature determined at compile time, call the method closest to the actual type of the object. The called method must exactly match the method signature determined at compile time. (Note that it’s not possible to have a runtime error in this step since we determined that at least some method could be called during compilation.)