CMPS 134/144/240
Case Study: Development of a Counter Class Hierarchy

Basic Counter Class

Suppose that you want to develop a class to model the behavior of a counter, i.e., an object with an integer value that you can increment, decrement, or set to a desired value. You might produce something like the following:

public class Counter {

   // instance variable
   // -----------------

   private int cntrVal;    // current value of the counter


   // constructors
   // ------------

   /* pre:  true
   ** post: this.countVal() == init
   */
   public Counter(int init) { this.cntrVal = init; }

   /* pre:  true
   ** post: this.countVal() == 0
   */
   public Counter() { this(0); }  // pass 0 to other constructor


   // observers
   // ---------

   public int countVal() { return cntrVal; }

   /* Returns a string indicating this counter's count value.
   */
   @Override
   public String toString() { return "Counter: " + countVal(); }


   // mutators
   // --------

   /* pre:  let c == this.countVal()
   ** post: this.countVal() == c + 1
   */
   public void increment() { cntrVal = cntrVal + 1; }

   /* pre:  let c == this.countVal()
   ** post: this.countVal() == c - 1
   */
   public void decrement() { cntrVal = cntrVal - 1; }

   /* pre:  true
   ** post: this.countVal() == newVal
   */
   public void setTo(int newVal) { this.cntrVal = newVal; }
}

Making a Reset-able Counter via Composition

Suppose that, after the class Counter has "gone into production" (and hence, by your company's rules, may not be modified), a need arises for a "reset-able" counter, i.e., one having a reset operation by which it can be made to return to its original value. We could develop a new class for this, making use of Counter as follows:

public class ResetableCounterViaComposition {

   // instance variables
   // ------------------

   private Counter c;   
   private int initVal;


   // constructors
   // ------------

   public ResetableCounterViaComposition(int initCntrVal)) {
      this.initVal = initCntrVal;
      this.c = new Counter(initCntrVal);
   }

   public ResetableCounterViaComposition() { this(0); }


   // observers
   // ---------

   public int countVal() { return c.countVal(); }

   /* Returns this counter's initial count value.
   */
   public int initialVal() { return initVal; }

   /* Returns a string indicating this counter's count value 
   ** and its initial value.
   */
   @Override
   public String toString() {
      return c.toString() + "; initially " + initialVal();
   }


   // mutators
   // --------

   public void increment() { c.increment(); }
   public void decrement() { c.decrement(); }
   public void setTo(int newVal) { c.setTo(newVal); }
   public void reset() { c.setTo(initVal); }
}

Notice that each instance of ResetableCounterViaComposition contains, as one of its fields (i.e., instance variables), a reference to an instance of Counter, named c. When an instance of a class A has as fields (references to) instances of classes A1, A2, ..., Ak, we say that A is built via composition from A1, A2, ..., Ak. The use of composition indicates that there is a has-a relationship between, in this case, instances of A and instances of each of the Ai's.

To illustrate what's going on, suppose that an instance of ResetableCounterViaComposition currently has value 7 (i.e., is in a state such that an invocation of countVal() yields 7) and had 0 as its initial value. Then the situation is as depicted below, in which there is an instance of ResetableCounterViaComposition (on the left) whose initVal field contains zero and whose c field contains a reference to an instance of Counter (on the right) whose cntrVal field has value 7:

     +-----------------+
     |         +---+   |
     | initVal | 0 |   |              +------------------+
     |         +---+   |              |          +---+   |
     |       c | *-|---+------------->|  cntrVal | 7 |   |
     |         +---+   |              |          +---+   |
     +-----------------+              +------------------+
        instance of                        instance of
ResetableCounterViaComposition               Counter

With the exception of reset(), every method in ResetableCounterViaComposition is implemented by invoking the same-named method on its instance variable c. Thus, even though each such method is "trivial" to implement, the fact remains that we have to supply the code for it.


Alternative: Using Inheritance rather than Composition

If, instead of viewing a reset-able counter as an object that contains a (plain) counter object as one of its components, we view it as a special kind of counter object, we obtain a more elegant result. The idea that every object of class B is a special kind of object of another class A is supported in Java (and other object-oriented languages) by the mechanism of inheritance, or subclassing, or extension. Here we would want to define B to be a subclass (or child class) of A. Using this mechanism, we extend Counter —note the use of the extends keyword in the class heading— to obtain a subclass ResetableCounter modeling a reset-able counter:

public class ResetableCounter extends Counter {

   // instance variable
   // -----------------

   private int initVal;   // initial count value 


   // constructors
   // ------------

   public ResetableCounter(int initCntrVal) {
      super(initCntrVal);     // calls parent class's constructor 
      this.initVal = initCntrVal;
   }

   public ResetableCounter() { this(0); }  // calls other constructor 

   // observers
   // ---------

   /* Returns this ResetableCounter's initial count value.
   */
   public int initialVal() { return initVal; }

   /* Returns a string indicating this counter's count value
   ** and its initial value.
   */
   @Override
   public String toString() {
      return super.toString() + "; initially " + initialVal();
   }

   // mutator
   // -------

   public void reset() { setTo(initVal); }
}

When a class B is built by extending a class A, we say that B is a subclass of A or, equivalently, that A is a superclass of B. (We also say that B is derived from A via extension and that B is the child class and A is its parent.) More concretely, what this means is that each instance of B has all the fields (and methods) that an instance of A has, plus whatever fields (and methods) are declared in B. We say that instances of B inherit the attributes/features (i.e., fields and methods) of class A. In particular, no declarations of attributes inherited from parent class A appear in (the source code of) child class B.

As an aside, we note that there are rules governing the accessibility of inherited attributes. For example, an instance variable (or method) declared to be private in class A cannot be referred to inside its child class B. In case we want an attribute (e.g., instance variable or method) to be accessible within its subclasses, we would use the access modifier protected rather than private in its declaration. (This also allows access by all the classes in the same package.)

In extending a class A to derive a subclass B, typically what we do is to introduce instance variables and methods that do not exist in A. In our example, ResetableCounter introduces an instance variable initVal and a method reset(). (Also, one or more constructors must be included in a subclass, unless it inherits a zero-argument constructor from its parent. It is typical for a child class's constructor to invoke a constructor of its parent and, in addition, to include code that initializes the values of the instance variables declared in the child class.)

If the behavior of a method that is inherited by a child class does not meet the need of that child class, the child class can override that method. Specifically, if B is a subclass of A, and B contains a method f() having the same name, return type, and signature (number and types of parameters) as some method in A (or inherited by A), we say that the method f() in B overrides the same-named method in A. What this means is that, if b is (a reference to) an instance of B, the statement b.f() will invoke B's version of method f() rather than A's.

The only inherited method that ResetableCounter overrides is toString(). Note that Counter itself overrides the version of toString() that it inherited from its parent, Object.) It is a good idea to include the compiler directive @Override just before the declaration of a method that overrides an inherited method. This is so the Java compiler can verify that its name, return type, and signature actually match that of an inherited method.


Extending ResetableCounter to RollOverCounter

Let us explore the issues of inheritance further by supposing that a need arises for a reset-able counter having specified minimum and maximum values and the property that its value "rolls over" when it is incremented beyond its maximum value or decremented beyond its minimum value. For example, on a clock, the hour rolls over from twelve to one and the minute from 59 to zero.

We could use either composition or inheritance to build this class. Taking the view that the kind of counter we are describing is a special case of a reset-able counter, we will use inheritance.

public class RollOverCounter extends ResetableCounter {

   // instance variables
   // ------------------

   private int minVal, maxVal; 


   // constructors
   // ------------

   /* pre:  min <= init <= max
   ** post: this.countVal() == init  &&
   **       this.minimumVal() == min  &&  this.maximumVal() = max
   */
   public RollOverCounter(int init, int min, int max) {
      super(init);     // invokes the parent's constructor
      this.minVal = min;
      this.maxVal = max;
   }

   /* pre:  min <= max
   ** post: this.countVal() == min  &&
   **       this.minimumVal() == min  &&  this.maximumVal() = max
   */
   public RollOverCounter(int min, int max) 
      { this(min, min, max); }


   // observers
   // ---------

   public int minimumVal() { return minVal; }
   public int maximumVal() { return maxVal; }


   // mutators
   // --------

   /* pre:  let c = this.countVal()
   ** post: this.countVal() == f(c), where
   **       f(k): if k == maximumVal() then minimumVal() else k+1
   */
   @Override
   public void increment() {
      if (countVal() == maximumVal())
         { setTo(minVal); }
      else
         { super.increment(); }  // invoke parent class's overridden method
   }


   /* pre:  let c = this.countVal()
   ** post: this.countVal() == g(c), where
   **       g(k): if k == minimumVal() then maximumVal() else k-1
   */
   @Override
   public void decrement() {
      if (countVal() == minimumVal())
         { setTo(maxVal); }
      else
         { super.decrement(); }  // invoke  parent class's overridden method
   }   

}

Notice that, within the class RollOverCounter, we have supplied new versions of the methods increment() and decrement(), which override the same-named methods inherited from its parent class, ResetableCounter (which itself inherited them from its parent, Counter). (Note the notation @Override that informs the reader (and the Java compiler) that these methods are intended to override the inherited versions.)

As this example illustrates, an overriding method in a subclass may invoke the method it overrides by inserting "super." before its name. It is not uncommon for an overridden method to be invoked within the method that overrides it; this makes sense, because often the overriding method carries out the same actions as the overridden method, plus additional actions. Here, the overriding method needs to do exactly what the overridden method does, except in some circumstances.


Refactoring to Introduce an Abstract Parent Class, BoundedCounter

Now suppose that we want a stopping counter that is similar to a rollover counter except that, where the latter would have rolled over, the former doesn't change. Suppose that we also want a warning counter that throws an exception in the same situations where the other two kinds of counters would have rolled over and stopped, respectively.

All three kinds of counters that were just mentioned —rollover, stopping, and warning counters— are similar; hence, rather than making the three corresponding classes be virtual copies of one another, we "factor out" their commonality and make an abstract class from which all three can be derived via extension. Because what is common about these three kinds of counters is that their values are bounded from below and above, we call this new class BoundedCounter:

public abstract class BoundedCounter extends ResetableCounter {

   // instance variables
   // ------------------

   private int minVal, maxVal; 


   // constructors
   // ------------

   /* pre:  min <= init <= max
   ** post: this.countVal() == init  &&
   **       this.minimumVal() == min  &&  this.maximumVal() = max
   */
   public BoundedCounter(int init, int min, int max) {
      super(init);
      minVal = min;
      maxVal = max;
   }

   /* pre:  min <= max
   ** post: this.countVal() == min  &&
   **       this.minimumVal() == min  &&  this.maximumVal() = max
   */
   public BoundedCounter(int min, int max) { this(min, min, max); }


   // observers
   // ---------

   public int minimumVal() { return minVal; }
   public int maximumVal() { return maxVal; }


   // mutators
   // --------

   /* pre:  let c = countVal()
   ** post: if c != maximumVal(), then same as overridden counterpart;
   **       if c == maximumVal(), then determined by subclass's
   **                             method incrementFromMax()
   */
   @Override
   public void increment() {
      if (countVal() == maximumVal())
         { incrementFromMax(); }
      else
         { super.increment(); }
   }


   /* pre:  let c = countVal()
   ** post: if c != minimumVal(), then same as overridden counterpart;
   **       if c == minimumVal(), then determined by subclass's
   **                             method decrementFromMin()
   */
   @Override
   public void decrement() {
      if (countVal() == minimumVal())
         { decrementFromMin(); }
      else
         { super.decrement(); }
   }


   // abstract methods that subclasses must define in order to
   // fully describe how increment() and decrement() behave
   
   protected abstract void incrementFromMax();
   protected abstract void decrementFromMin();
}

Notice that BoundedCounter is declared to be abstract. This means that no object may be constructed directly from this class. Here, this makes sense, because even though the class provides its clients with a means of establishing boundaries upon a counter, it provides no specific means of enforcing them! More precisely, it assumes that a counter whose value is not at its maximum (respectively, minimum) allowed value should respond "normally" (i.e., as specified in its superclass, ResetableCounter) when incremented (respectively, decremented), but it yields to its subclasses the responsibility to define, via the method incrementFromMax() (respectively, decrementFromMin()) how a counter should respond to being incremented (respectively, decremented) when its value is at the maximum bound (respectively, minimum bound). This explains the presence of these two abstract methods.

Having defined BoundedCounter as above, the class RollOverCounter can be simplified to what is shown below left and our hierarchy of counter classes —imagining that StoppingCounter and WarningCounter have been developed as well— can be depicted as shown below right. (Note that an arrow points from a child class to its parent and a "dotted" rectangle indicates an abstract (rather than concrete) class.)

public class RollOverCounter extends BoundedCounter {

   // constructors
   // ------------

   public RollOverCounter(int init, int min, int max) 
      { super(init, min, max); }

   public RollOverCounter(int min, int max) { super(min, max); }


   /* concrete definitions for abstract methods in superclass */

   protected void incrementFromMax() { setTo(minimumVal()); }

   protected void decrementFromMin() { setTo(maximumVal()); }

}
              +---------+
              | Counter |
              +---------+
                   ^
                  / \
                  ---
                   |
                   |
                   |
         +------------------+
         | ResetableCounter |
         +------------------+
                   ^
                  / \
                  ---
                   |
                   |
                   |
          +................+
          : BoundedCounter :
          +................+
                   ^
                  / \
                  ---
                   |
                   |
                   |
     +-------------+-------------+
     |             |             |
     |             |             |
+----------+  +----------+  +---------+
| RollOver |  | Stopping |  | Warning |
| Counter  |  | Counter  |  | Counter |
+----------+  +----------+  +---------+


Exercise 1: Using the latest version of RollOverCounter (immediately above) as a model, develop the classes StoppingCounter and WarningCounter.

Exercise 2: Consider this code segment:

Counter rc = new RollOverCounter(0, 10);
rc.setTo(25);
The result is that rc, which is supposed to be an instance of RollOverCounter having as its range of possible values 0..10, will have value 25. Of course, the same thing could happen with an instance of StoppingCounter or WarningCounter, assuming (as suggested in the previous exercise) that they are modeled after RollOverCounter. Modify BoundedCounter so as to ensure that any invocation of setTo() upon any instance of any of its descendant classes will not allow that object's value to be set to a value outside its legal range of values. (One approach would be to throw an IllegalArgumentException when appropriate.)


Polymorphism and Dynamic Method Invocation

To illustrate the concepts of polymorphism and dynamic method invocation (which are characteristic of object-oriented programming languages), consider the following Java application, which includes the (rather contrived) method countSpaces() that, given a String object and a Counter object, scans the characters in the former, invoking the latter's increment() method each time a space character is encountered.

The application's main() method calls the countSpaces() method five times, passing to it the same string each time, but a different kind of counter object each time. (Each counter object has a count of zero when it is passed.)

After each call to countSpaces(), the value returned by the relevant counter's countVal() method is printed. The embedded comments describe what those values should be, and why.

public class CounterTester {

   public static void main(String[] args)
   {
      Counter counter;
      String s = "The cat in the hat ate a rat."; 

      counter = new Counter(0);
      countSpaces(s, counter);
      System.out.println("The (plain) Counter says " + counter.countVal());
      // Result printed should be 7, as there are seven spaces in s.

      counter = new ResetableCounter(0);
      countSpaces(s, counter);
      System.out.println("The ResetableCounter says " + counter.countVal());
      // Result printed should be 7, as there are seven spaces in s.

      counter = new RollOverCounter(0,0,4);
      countSpaces(s, counter);
      System.out.println("The RolloverCounter says " + counter.countVal());
      // Result printed should be 2. (The 5th space takes the counter back to
      // zero, because 4 is the counter's upper bound.  The remaining two
      // spaces take the counter back up to two.)

      counter = new StoppingCounter(0,0,4);
      countSpaces(s, counter);
      System.out.println("The StoppingCounter says " + counter.countVal());
      // Result printed should be 4, as incrementing the counter has no
      // effect once it has reached its upper bound of 4.

      counter = new WarningCounter(0,0,4);
      countSpaces(s, counter);
      // The above call results in an exception being thrown, as on the
      // fifth space an attempt would be made to increment the counter
      // beyond its upper bound of 4.
      System.out.println("The WarningCounter says " + counter.countVal());
   }


   /* Scans the given string and, for each occurrence of a space character
   ** therein, invokes the increment() method upon the given Counter object.
   */
   public static void countSpaces(String str, Counter cntr) {
      for (int i=0; i!= str.length(); i++) {
         if (str.charAt(i) == ' ') { 
            cntr.increment();
         }
      }
   }

Notice that during execution of the main() method, its local variable counter, which is declared to be of type Counter, initially refers to an instance of Counter, then is modified so that it refers to an instance of ResetableCounter, then to a RollOverCounter, then to a StoppingCounter, and, finally, to a WarningCounter. Similarly, the formal parameter cntr of the countSpaces() method, also declared to be of type Counter, refers to an instance of a different class each time it is called.

What this illustrates is that a variable1 that is declared to be of (reference) type A can have as its value a reference to an instance of any class that is a descendant of class A (including A itself, of course). More generally, one could say that, anywhere that it is "legal" to use an instance of A, it is also legal to use an instance of any of A's descendants. This rule (or capability, if you prefer) is called (subtype) polymorphism.

Now consider the countSpaces() method, which includes the method call cntr.increment(). Of course, the effect of this call is to invoke the increment() method upon the object referred to by cntr, which is (a formal parameter) declared to be of type Counter.

The question is, "Which version of increment() will be called?"2 It would be reasonable to think that, because cntr is declared to be of type Counter, the increment() method in that class should be the one. But that is not how it works! Rather, the version of increment() that is called is the one that is "appropriate" to the kind of object that cntr refers to. The following table summarizes.

When cntr refers to an
instance of this class
the version of increment()
called is the one found in
CounterCounter
ResetableCounterCounter
RollOverCounterBoundedCounter
StoppingCounter BoundedCounter3
WarningCounter BoundedCounter4

This rule is called dynamic method invocation (or dynamic dispatch).

To appreciate that the combination of polymorphism and dynamic method invocation is a powerful tool in the quest to produce reusable and extendible code, and to avoid the introduction of redundant code, consider that, without them, the Java application shown above would have to be rewritten to have five almost identical versions of the countSpaces() method, differing only in that one of them would declare cntr to be of type Counter, another would declare cntr to be of type ResetableCounter, another would etc., etc. And, if in the future a new subclass of Counter were developed, and we wanted to be able to use its instances to "count spaces", a corresponding new version of the countSpaces() method would have to be added, too, differing from the existing ones only in the declared type of cntr.

In some cases, dynamic method invocation occurs not only in deciding which version of the increment() method to execute, but also during execution of the chosen version. Suppose, for example, that the increment() method is invoked upon an instance of the RollOverCounter class. As RollOverCounter inherits that method from BoundedCounter, the version of increment() that is chosen to be executed is the one found inside BoundedCounter.

Suppose that the counter's value happens to be at its upper limit. In that case, the first branch of the if-else statement applies, and it calls the incrementFromMax() method. But each of BoundedCounter's child classes has such a method, so which of them is actualy called? Not surprisingly, it is the one found in RollOverCounter, which is only appropriate, since the object being incremented is an instance of that class!

If the object upon which increment() was called was instead an instance of StoppingCounter, then the version of incrementFromMax() to be called would be the one found in that class.3


Footnotes

[1] or a formal parameter, which is a special case of a variable

[2] There are at least two different methods named increment() among the classes that are descendants of Counter, one in Counter itself and one in its grandchild BoundedCounter.

[3] This assumes that StoppingCounter inherits the version of increment() found in its parent class BoundedCounter, as its sibling RollOverCounter does.

[4] This assumes that WarningCounter inherits the version of increment() found in its parent class BoundedCounter, as its sibling RollOverCounter does.