Inheritance

Inheritance and Subclasses | Super | Constructors | Abstract Classes and Methods | References and Objects | Casting | Overriding Methods




Inheritance and Subclasses

The notion of inheritance is one of a hierarchy, that some sort of ordering or subset operations can be imposed on certain classes that may help us to write very flexible code more efficiently by takeing advantage of similarities. For example, all dogs are animals. Anything a generic animal could do, a dog should be able to do as well.

In Object-Oriented Programming, we formalize these so-called "is-a" relationships with the notion of inheritance. We say that class Dog inherits from class Animal. Terminology here is varied. Base class, parent class, and ancestor class all refer to classes further up the inheritance hierarchy while derived class, child class, and descendent class all refer to classes that are further down the inheritance hierarchy. Note that as you go up, the definition gets more general, and as you go down, the classes become more specific.

To generate a subclass from another class, we use the keyword extends in the class declaration line as follows:
BaseClass
public class BassClass {
	:
	:
}
DerivedClass
public class DerivedClass extends BassClass {
	:
	:
}
Because we have developed new relations for classes other than the distinction of outside class, we need to define a new visibility modifier. Now we have three categories: To accomodate this, the visibility modifier protectedwill be used to declare members that the class and any subclasses may have access to, but no other classes. Private members are not inherited. Thus, the protected visibility is in between public and private.

When one class inherits from another, we can assume that it will also have all non-private data and method members (except the constructor). In this way, we maintain the analogy that a Dog can do anything an Animal can do.



Super

In a similar way that the keyword this can be used by an object to call overloaded methods, the keywor super can be used by subclasses to call overriden methods of their immediate parent class. Here's a very short example:

High.java
public class High {

  public High() {}

  public void print() {
    System.out.println("high");
  }

}
Middle.java
public class Middle extends High {

  public Middle() {}

  public void print() {
    System.out.print("middle ");
    super.print();
  }

}
Low.java
public class Low extends Middle {

  public Low() {}

  public void print() {
    System.out.print("low ");
    super.print();
  }

}
Main.java
public class Main {

  public static void main(String[] args) {

    (new High()).print();
    (new Middle()).print();
    (new Low()).print();

  }

}
output
high
middle high
low middle high













Constructors

Despite their visibility modifiers, constructors are never inherited. The reason for this is that in order to inherit a method, the method signature, which includes the method name, must be identical. Since we know that the name of constructors are the same name as the class, and that all classes have different names, it wouldn't make sense fot constructors to be inherited.

However, we can stil use the keyword super in a similar way as the keyword this. The keyword super, if executed, Therefore, we can say write the following for XYPoints and XYZPoints:

XYPoint.java
public class XYPoint {

  protected int x;
  protected int x;

  public XYPoint(int x, int y) {
    this.x = x;
    this.y = y;
  }

}
XYPoint.java
public class XYZPoint {

  protected int z;

  public XYPoint(int x, int y, int z) {
    super(x,y);
    this.z = z;
  }
   
}
Note that, just like the keyword this, we call it like a method name for constructors but use it as an object reference for a scope when calling any other members.



Abstract Classes and Methods

If a class is declared abstract, If a method is declared as abstract, Abstract methods are useful when you know that subclasses will somehow perform a task, but they will perform it in very different ways. Here is a small example of using the keyword abstract:

AbstractWanderer.java
public abstract class AbstractWanderer {

  protected Location location;
  protected boolean direction;
   
  public abstract void move();

  protected int random(int low, int high) {
    return (int)(Math.random()*(high-low+1))+low;
  }
 
  protected int booleanToInt(boolean b) {
    if (b)
      return 1;
    else
      return -1;
  }

  public String toString() {
    return "A Wanderer at " + location;
  }

}
NorthSouthWanderer.java

public class NorthSouthWanderer
        extends AbstractWanderer {

  public void move() {
    int dx = random(-1,1);
    int dy = booleanToInt(direction);
    location.moveOneSpace(dx,dy);
  }

}
EastWestWanderer.java
public class EastWestWanderer
        extends AbstractWanderer {

  public void move() {
    int dx = booleanToInt(direction);
    int dy = random(-1,1);
    location.moveOneSpace(dx,dy);
  }

}




Object References and Objects

The intended goal of inheritance is to reuse code in a way that fosters polymorphism. Polymorphism means treating different objects in a similar way. For computing, this means having expectations about what members belong to objects.

Examine the following classes:

Animal.java
public class Animal {

  protected void makeNoise() {
    System.out.println("Don't know what noise to make.");
  }

}
Dog.java
public class Dog extends Animal {

  public void makeNoise() {
    bark();
  }

  public void bark() {
    System.out.println("bark");
  }



Now let's say we wish to create an object and assign it to a reference variable. Since we have two classes, there are four possible ways to do this. In order to compile, we will need to make to make sure that the actual object has all the methods that the reference expects it to have. In other words, whenever we have an Animal reference, we should always be able to call makeNoise() on the object to which it refers. Whenever we have a Dog reference, we should always be able to call both makeNoise() and bark() on the object to which it refers.

Here is a table of possible assignments. The illegal one is highlighted in red.

object tpye
Animal Dog
reference type Animal
Animal aa = new Animal();
aa.makeNoise();
Animal ad = new Dog();
ad.makeNoise();
Dog
Dog da = new Animal();
da.makeNoise();
da.bark(); illegal
Dog dd = new Dog();
dd.makeNoise();
dd.bark();


As a general rule, object references must be at least as high up on the inheritance hierarchy as the actual object to which they refer.



Casting

When using object references that are less specific than the actual object to which they refer, a time may come when you need to treat that object as it's more specific type. We can do this by explicit casting, but we may generate a ClassCastException, which is thrown whenever an illegal explicit cast is attempted. Here is an exmaple:

Main.java
public class Main {

  public static void main(String[] args) {
    Animal aa = new Animal();
    try {
      ((Dog)aa).bark();
    } catch (ClassCastException cce) {
      System.out.println("the object to which "
                   + "aa referes is not a Dog!");
    }
  }

}
console output
the object to which aa referes is not a Dog!


A keyword operator (which breaks convention in several ways) called instanceof performs a boolean test to see if the object to which a reference refers is an instance of a particular class. This way, we can program defensively and avoid excess exception handling. Here is an example:

Main.java
public class Main {

  public static void main(String[] args) {
    Animal ad = new Dog();
    if (ad instanceof Dog) {
      ((Dog)ad).bark();
    }
  }

}
console output
bark


Note that this operator will return false for null reference pointers, regardless of type.



Overriding Methods

We know that to have a subclass override an inherited method from its parent class, it must have access to it, it needs to provide at least as visible a visibility modifer, the same return type, the same method name, the same parameter list. Any variation on this will fail either to compile or to succesfully override the method. See makeNoise() in the example above.

Please refer to the following link for a more explicit
table of rules on this confusing subject.

But how do we know which method will be called, if we're allowed to use object references that are higher in the hierarchy chain than the actual object itself? In other words, what is the output of the following code, since no casting is being performed?

main class
 
calling Animal.makeNoise()
 
calling Dog.makeNoise()
 
Main.java
public class Main {

  public static void main(String[] args) {
    Animal ad = new Dog();
    ad.makeNoise();
  }

}
==>
console output
Don't know what noise to make.
or
console output
bark
?


Well, the answer is that JAVA will automatically execute the most specific method applicable to that object. This means that even though the object reference is of type Animal, because the actual object is a Dog, Dog's makeNoise() method will be called and the output will be "bark."

If we think about this, it makes sense. If the computer did not automatically call the subclass' methods, then there wouldn't be any advantage to having subclasses. The key here is to treat many subclasses of the same base class uniformly, but still have them act differently. Here's an example:

Say your boss wants you top write a Collection class with two methods, add and access. You know from good programming skills that you can either implement your code as an array or as a linked list. The array will always be very slow to add but very fast to access. The linked list will be very fast to add but very slow to access. It would be nice to know whether you will be doing more adds or more accesses, but your boss didn't tell you. So you use inheritance and write both as follows:
Collection.java
public abstract class Collection {

  private int size;

  public Collection() {
    size = 0;
  }

  public int size() {
    return size;
  }

  public void add(Object data) {
    ++size;
  }

  public abstract Object access(int index);

  public String toString() {
    StringBuffer buffer = new StringBuffer("<");
    for (int i = 0; i < size(); ++i) {
      buffer.append(" " + access(i));
      if (i < size() - 1)
        buffer.append(",");
    }
    buffer.append(" >");
    return buffer.toString();
  }

}
FastAccessCollection.java
public class FastAccessCollection extends Collection {

  private Object[] array;

  public FastAccessCollection() {
    super();
    array = new Object[size()];
  }

  public void add(Object data) {
    super.add(data);
    Object[] biggerArray = new Object[size()];
    biggerArray[0] = data;
    for (int i = 1; i < size(); ++i) {
      biggerArray[i] = access(i-1);
    }
    array = biggerArray;
  }

  public Object access(int index) {
    return array[index];
  }

}
FastAddCollection.java
public class FastAddCollection extends Collection {

  private final ListNode first;

  public FastAddCollection() {
    super();
    first = new ListNode(null);
  }

  public void add(Object data) {
    super.add(data);
    ListNode dataNode = new ListNode(data);
    dataNode.setNext(first.getNext());
    first.setNext(dataNode);
  }

  public Object access(int index) {
    ListNode marker = first;
    int i = 0;
    while (i <= index) {
      marker = marker.getNext();
      ++i;
    }
    return marker.getData();
  }

  class ListNode {

    private ListNode next;
    private final Object data;

    ListNode(Object data) {
      this.data = data;
      next = null;
    }

    public Object getData() {
      return data;
    }

    public ListNode getNext() {
      return next;
    }

    public void setNext(ListNode next) {
      this.next = next;
    }

  }

}


Now in order to choose between which subclass of Collection to use, all you have to do is change one line of your test file - the object instantiation line. That's why polymorphism through inheritance is powerful. Examin how similar the two options are:

Test.java
public class Test {

  public static void main(String[] args) {
    Collection c = new FastAccessCollection();
    c.add(new Integer(5));
    c.add(new Integer(4));
    c.add(new Integer(3));
    c.add(new Integer(2));
    c.add(new Integer(1));
    System.out.println(c.access(3));
  }

}
Test.java
  public class Test {

    public static void main(String[] args) {  
      Collection c = FastAddCollection();
      c.add(new Integer(5));
      c.add(new Integer(4));
      c.add(new Integer(3));
      c.add(new Integer(2));
      c.add(new Integer(1));
      System.out.println(c.access(3));
    }

  }