Inheritance II

Homework review

The homework from the previous class is really important, as it examines the mechanisms of inheritance, constructors for derived classes, and overloading vs. overriding for derived classes.

Another "extending functionality" example: adding size() to Queue

As a starting point, let's suppose we have the class Queue (from a previous homework), along with a simple testing program Ex1 (note: to finish entering input for Ex1, hit ctrl-d). We don't give you the Queue code since that is on a HW and possible Project solution, but you don't actually need it to implement inheritance!

Queue.java
public class Queue {

  public void enqueue(String s) {
    // valid code for adding a string
  }

  public String dequeue() {
    // valid code for removing a string
  }

  public boolean empty() {
    // valid code for testing if the queue is empty
  }

  private class Node {
    public String data;
    public Node next;
    public Node(String d, Node n) { data = d; next = n; }
  }

  private Node head = null, tail = null;
}
Ex1.java
import java.util.*;

public class Ex1 {

  public static void main(String[] args) {
    Queue Q = new Queue();
    Scanner sc = new Scanner(System.in);
    while( sc.hasNext() ) {
      Q.enqueue(sc.next());
    }
    while( !Q.empty() ) {
      System.out.println(Q.dequeue());
    }
  }
}
Suppose we sometimes want to have Queues of Strings that at any point in time can tell us how many elements are currently in the Queue. Once again, we want to do this without changing the original Queue class. This might be because we only occasionally need this new functionality, or it might be because we don't have the right to change Queue, or because we don't actually have access to the source code. Here's how we might do it.

CountingQueue.java
public class CountingQueue extends Queue {

  private int count;

  public void enqueue(String s) {
    count++;
    super.enqueue(s);
  }

  public String dequeue() {
    count--;
    return super.dequeue();
  }

  public int size() { return count; }
}
Ex2.java
import java.util.*;

public class Ex2 {

  public static void main(String[] args) {
    if (args.length == 0) {
      System.out.println("usage: java Ex2 ");
      System.exit(0);
    }
    int N = Integer.parseInt(args[0]);
    boolean flag = true;
    CountingQueue Q = new CountingQueue();
    Q.enqueue("");
    while(!Q.empty()) {
      String s = Q.dequeue();
      System.out.println(s);
      flag = flag && Q.size() < N;
      for(int i = 0; flag && i < 26; i++) {
        Q.enqueue(s + (char)('a' + i));
      }
    }
  }
}

Why not super.super?

A question that got asked, in one form or another, several times, was whether or not we could make a call like "super.super.foo()", in order to get the function not from the super class, but from the super class's super class. The answer is "No!", but we're in a good position to understand why that would be undesirable. Suppose I was able to do this, and I produced the following code:

public class MyQueue extends CountingQueue {
   public void enqueue(String s) {
      super.super.enqueue("my:"+s); ← Not actually legal!!
   }
   ...
}
By doing this, the "CountingQueue" functionality that I was supposed to have inherited has been messed up! Why? Because something has been enqueued but not "counted". Moreover, it's only possible to know whether or not this has messed something up, and in what way it's been messed up, if you are intimately familiar with the implementation of CountingQueue, and that's the very thing we don't want in object oriented programming!

If you don't think this is bad, consider the following: what if the person in charge of CountingQueue decides to override empty() so that it checks whether count is zero to provide it's true/false answer. From the CountingQueue guy's perspective, this should be thoroughly safe and not break other people's code. But now MyQueue will break in an unanticipated way: done() for MyQueue's will give incorrect results.

That's why super.super, or something like it, is not allowed!

A different extension, and the need for protected access

A really useful function to have with Queues is "peek", which tells you what's at the front of the Queue without actually removing it. Peek allows you to "look before you leap", so to speak, because once you dequeue(), you can't put what you've just removed back in front again. At first blush, it seems that peek() should be easy: all we have to do is extend Queue and add one little method.

PeekaQueue.java
public class PeekaQueue extends Queue {
  public String peek() {
    ? ? ? ;
  }
}

But how do we define that method? The super class Queue does not provide public methods that allow us to implement this functionality, and since the fields of Queue are private, we can't implement it for ourselves. From the perspective of the implementer of class Queue, you would say that it's impossible to anticipate everything someone would want to do with a Queue and to provide public methods that make all possible operations implementable. That's true. A compromise between strict separation of interface from implementation and a wild west all-fields-are-public approach is to use the protected access modifier. When a field is marked "protected", it means that it is accessible inside the class and any derived classes, but not from anywhere else. If we change head and tail in Queue to be protected, implementing PeekaQueue becomes easy.

Queue.java
public class Queue {
  public void enqueue(String s) {
    // valid code for adding a string
  }

  public String dequeue() {
    // valid code for removing a string
  }

  public boolean empty() {
    // valid code for testing if the queue is empty
  }

  private class Node {
    public String data;
    public Node next;
    public Node(String d, Node n) { data = d; next = n; }
  }

  protected Node head = null, tail = null;
}
PeekaQueue.java

public class PeekaQueue extends Queue {
  public String peek() {
    return head == null ? null : head.data;
  }
}