CMPS 134/144
Inheritance Example: Coin and Descendants

Background

One of the defining features of object-oriented programming (and the languages that support it) is inheritance, one goal of which is to promote code reuse (which is the opposite of code redundancy). Among the software engineering principles that most closely relate to inheritance is the Open-Closed Principle, named by prominent computer scientist Bertrand Meyer. Altering its language to be specific to Java, it says

Open-Closed Principle: Java classes should be open for extension, but closed for modification.

In a strict sense, being "closed for modification" means that —once a class has been "put into production"— its source code cannot be changed. One might reasonably relax this restriction to allow changes that are transparent to client programs, meaning that any modifications preserve the observable behavior of (instances of) the class. (In other words, changes are restricted to ones that effect only implementation details, while the class's "interface" remains stable.)

As for the "open" part of the principle, a class C is said to be open for extension if it is possible to define a new class C', referred to as a child of class C, whose instances

  1. can be used anywhere that instances of class C can be used (implying, for example, that a variable of type C can have as a value a reference/pointer to an instance of C'), and
  2. can (and generally do) have "extra" functionality not possessed by instances of C. For example, C' might include public methods not found in C.

An Example: Coin and its Descendants

Consider the following Java class, instances of which represent fair tossable coins. (A coin is said to be "fair" if, on any given toss, the likelihood of the result being HEADS is the same as the likelihood of it being TAILS.)

Figure 1: Coin Class
import java.util.Random; 

/* An instance of this class represents a (fair) coin that 
** is suitable for simulating a sequence of coin tosses.
*/
public class Coin {

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

   protected boolean isHeads;  // true if HEADS is "showing"
   protected Random rand;      // for producing pseudo-random numbers 
                               // that simulate coin tosses

   // constructor
   // -----------

   /* Establishes this coin as one that is showing HEADS.
   */
   public Coin() { 
      rand = new Random();
      isHeads = true;
   }


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

   /* Reports whether or not this coin is showing HEADS.
   */
   public boolean isHeads() { return isHeads; }

   /* Reports whether or not this coin is showing TAILS.
   */
   public boolean isTails() { return !isHeads(); }

   /* Returns a string indicating which face this coin is showing.
   */
   public String toString() { 
      return isHeads() ? "HEADS" : "TAILS";
   }


   // mutator
   // -------
   
   /* Tosses this coin, producing each of the two 
   ** possible results with equal probability.
   */
   public void toss() { 
      // Pseudo-random numbers in the range [0.0 .. 0.5) 
      // map to HEADS, those in [0.5 .. 1.0) map to TAILS.
      final double ONE_HALF = 1.0 / 2.0
      isHeads = rand.nextDouble() < ONE_HALF;
   }
}

Suppose that, sometime after Coin has been established as a "working class" (and hence it has been deemed "closed" for modification), the need arises for the ability to simulate tosses of biased (i.e., unfair) coins. What do we do? Apparently, we need a new class. A reasonable name for such a class would be BiasedCoin.

Perhaps the most obvious way to build the BiasedCoin class would be to take the source code of Coin and make whatever additions, removals, and changes are necessary to achieve the behavior we desire. Clearly, the toss() method would require an overhaul, because it is designed specifically to produce HEADS and TAILS with equal probability. Assuming, as is reasonable, that the client program should be able to specify the desired probabilities with which each of HEADS and TAILS occur, some mechanism for doing that must be provided, either via a mutator method or a constructor.

It turns out that we don't need to duplicate the code in Coin in order to develop BiasedCoin. Rather, we can make BiasedCoin extend Coin, meaning that all the data and method declarations in the latter are inherited by the former (and need not be explictly duplicated therein). When a class B is declared to extend a class A, we describe their relationship by referring to B as a child of A and to A as being the parent of B. It is also common to use the terms subclass and superclass rather than, respectively, child and parent.

As a consequence of inheritance, if the entirety of the BiasedCoin class were

Figure 2: Empty BiasedCoin Class
public class BiasedCoin extends Coin { }

then instances of BiasedCoin would be exactly like instances of Coin, having the same instance variables and the same collection of methods.

Now, because we want instances of BiasedCoin to be biased (at least potentially), and we want the client program (i.e., the one that creates an instance of the class) to specify the degree of biasedness, we introduce a constructor with this specification:

Figure 3: Specification of BiasedCoin Constructor
/* Establishes this coin as one such that each toss has
** the specified probability of resulting in HEADS.
** pre: 0.0 <= probOfHeads <= 1.0
*/
public BiasedCoin(double probOfHeads) { ... }

Because the only outcome of a coin toss that is distinct from HEADS is TAILS, if we know the probability of one, we know the probability of the other, and hence there is no reason for a second formal parameter. (We ignore the possibility of a coin standing on its edge after it is tossed!)

In any case, each instance of BiasedCoin needs to store one or more pieces of data (in instance variables) that somehow relate to these intended probabilities and that can be used, in conjunction with a pseudo-random number generator, to produce the two possible outcomes with the intended relative frequencies.

Observe the toss() method in Coin. It uses the nextDouble() method of the instance variable rand (which refers/points to an instance of the java.util.Random class) to produce a pseudo-random number in the interval [0..1), and then it interprets that number as indicating a result of HEADS (respectively, TAILS) if it is in the subinterval [0 .. 1/2) (respectively, [1/2 .. 1)).

Because the values produced by the nextDouble() method are —at least approximately— "uniformally distributed" over the interval [0..1), half the time it will produce a number in [0 .. 1/2) (hence, a toss resulting in HEADS) and similarly for [1/2 .. 1) (TAILS).

If the "boundary" separating the two subintervals were, say, 3/5 rather than 1/2, then (approximately) 3/5 of the time the result would be HEADS. Generalizing this observation, and applying it, we recognize that a good way to achieve the goal of producing HEADS and TAILS with frequencies consistent with the value passed to the constructor —which is the desired probability of HEADS being the result of a toss— is to store that value in an instance variable and then to use it as the boundary between the subintervals of [0..1) that map to HEADS and TAILS, respectively.

But coin tosses are simulated by the toss() method, so this means that BiasedCoin must override the version of that method that it inherits from its parent, Coin. All of this gets us to the following:

Figure 4: BiasedCoin (2nd version)
public class BiasedCoin extends Coin {

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

   protected double headsProb;  // probability of HEADS being tossed
   

   // constructor
   // -----------

   /* Establishes this coin as one such that each toss has the
   ** specified probability of resulting in HEADS.
   ** pre: 0.0 <= probOfHeads <= 1.0
   */
   public BiasedCoin(double probOfHeads) { 
      // call the parent's constructor to initialize
      // the inherited instance variables
      super();

      // initialize the instance variable introduced here
      this.headProbs = probOfHeads;
   }


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

   @Override
   public void toss() {
      isHeads = rand.nextDouble() < headsProb;
   }
}

A subtle point here is that the "new" toss() method —which overrides the inherited version, as indicated by the compiler directive @Override— refers to the inherited instance variables isHeads and rand. That is possible only because those variables were not declared to be private in the parent class. Rather, they were declared to be protected, meaning that they can be referred to not only within the Coin class itself but also within any descendant class (child, grandchild, etc.) of Coin. (For that matter, entities within a class that are not declared to be private can be referred to by any class in the same package.)

What if rand had been declared to be private in the parent class? Then we would have had little choice but to declare a new instance variable within BiasedCoin to play the same role. Which means that instances of the BiasedCoin class would be carrying around an inherited instance variable that is never used and hence serves no purpose, which would be undesirable.

Even worse would have been for isHeads to be inaccessible to BiasedCoin, for then it would have been necessary to introduce a new instance variable to play the same role and to override not only toss() but also isHeads(). At that point, it would have made more sense to develop BiasedCoin from scratch.

This is why, when designing a Java class, it is important to foresee the possibility of it being extended, so that instance variables that may "need" to be accessed directly within child classes (or more distant descendants) are declared to be protected rather than private. (Unfortunately, such variables would then also be accessible by other classes in the same package, which may not be what is intended. For whatever reason, the designers of Java did not provide a way to declare an entity so that its scope includes descendant classes but not non-descendant classes in the same package.)

Polymorphism and Dynamic Method Dispatch

To illustrate these concepts of object-oriented programming, consider the following program.

Figure 5: BiasedCoinTester1
 (1) public class BiasedCoinTester1 {
 (2)    public static void main(String[] args) {
 (3)       Coin c;

 (4)       c = new Coin();        // simulate coin tosses using
 (5)       doSomeTosses(c, 20);   // an instance of Coin

 (6)       c = new BiasedCoin(0.1);  // simulate coin tosses using
 (7)       doSomeTosses(c, 20);      // an instance of BiasedCoin
 (8)    }

        /* Tosses the given "coin" the specified # of times; afterwards
        ** reports how many of those tosses resulted in HEADS and how
        ** many resulted in TAILS.
        */
 (9)    private static void doSomeTosses(Coin coin, int numTosses) {
(10)       int headCount = 0;
           // Toss the provided coin the specified # of times.
(11)       for (int i=0; i != numTosses; i++) {
(12)          coin.toss();
(13)          if (coin.isHeads()) { headCount++; } 
(14)       }
           // Report how many HEADS and TAILS occurred.
(15)       System.out.printf("# Heads: %d; # Tails: %d\n", 
                             headCount, numTosses - headCount);
(16)    }
(17) }

Note: As a matter of terminology, we will say that a variable is bound to an object if its value is a reference/pointer to that object. Thus, an assignment statement has the effect of binding a variable to an object, and a call to a method binds its formal parameters to the actual parameters listed in the call. End of note.

Notice that variable c is declared (on line 3) to be of type Coin, meaning that it can be bound to an instance of any descendant of the Coin class (including Coin itself). Observe that, initially, the program binds it to an instance of Coin (line 4), but later (line 6) binds it to an instance of Coin's child BiasedCoin. The ability of a variable to be bound to instances of various classes at various times is called polymorphism.

A similar observation applies to coin, the formal parameter of the doSomeTosses() method. The first call to this method (line 5) binds an instance of Coin to this parameter, but the second call (line 7) binds an instance of BiasedCoin to it.

Consider what happens (during execution of doSomeTosses()) as a result of the call coin.toss() (line 12). As previously noted, coin is of type Coin. During the execution of the method resulting from the call in line 5, coin is bound to an instance of the Coin class. Not surprisingly, then, the call coin.toss() on line 12 will invoke the toss() method found in the class Coin.

But what about the execution of doSomeTosses() instigated by the call to it in line 7? During that execution, coin is bound to an instance of the BiasedCoin class.
Question: Which version of toss() will be executed as a result of the call on line 12 in this case?
Answer: The version in BiasedCoin (that overrides the inherited version)!

To generalize, suppose that an object x is a direct instance of class Z (meaning that it is an instance of Z but not of any proper descendant of Z). Then when a method y() is invoked upon that object (as in the call x.y()), the version of y() that executes is the one declared in class Z, if there is one, or, if not, then the version of y() inherited by Z. This is called dynamic method dispatch.

Figure 6: Sample Output of
Run of BiasedCoinTester1
# Heads: 9; # Tails: 11
# Heads: 3; # Tails: 17
In non-technical terms, what dynamic method dispatch ensures is that the version of a method that gets executed is the one that is "most appropriate" to the object upon which the method is called. Figure 6 illustrates what a typical run of BiasedCoinTester1 might produce as output. Notice that the coin tosses simulated by the first call to doSomeTosses() (using an instance of the Coin class) result in an approximately equal number of HEADS and TAILS whereas the tosses simulated by the second call to that method (which uses an instance of BiasedCoin that is intended to produce HEADS only 10% of the time (see call to constructor in line 6)) produces only three HEADS out of 20.

Enhancements to BiasedCoin

Let us enhance the BiasedCoin class in two ways, one of which is to include methods that return the probabilities with which HEADS and TAILS should occur. The other is to include a zero-argument constructor that produces a fair coin. In effect, this means that a "default" instance of BiasedCoin (i.e., one constructed without the client program specifying a probability for HEADS) will be fair. We get this updated version of the class (in which the new code segments are shown in blue):

Figure 7: BiasedCoin (3rd version)
public class BiasedCoin extends Coin {

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

   protected double headsProb;  // probability of HEADS being tossed

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

   /* Establishes this coin as one such that each toss has the
   ** specified probability of resulting in HEADS.
   ** pre: 0.0 <= probOfHeads <= 1.0
   */
   public BiasedCoin(double probOfHeads) { 
      // call the parent's constructor to initialize
      // the inherited instance variables
      super();

      // initialize the instance variable introduced here
      this.headsProb = probOfHeads;
   }
    
   /* Establishes this coin as one that is fair.
   */
   public BiasedCoin() { 
      // Call the other constructor and pass a value to it
      // that results in each outcome having probability 1/2.
      this(0.5);
   }


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

   /* Returns the probability with which a 
   ** given toss will result in HEADS
   */
   public double headsProbability() { return headsProb; }

   /* Returns the probability with which a 
   ** given toss will result in TAILS
   */
   public double tailsProbability() 
      { return 1.0 - headsProbability(); }
   


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

   @Override
   public void toss() {
      isHeads = rand.nextDouble() < headsProb;
   }
}

Consider this program:

Figure 8: BiasedCoinTester2
(1) public class BiasedCoinTester2 {
(2)    public static void main(String[] args) {
(3)       Coin c = new BiasedCoin(0.45);
          // Java compiler rejects next line!
(4)       double probOfHeads = c.headsProbability();
(5)    }
(6) }

The Java compiler rejects this program. Why? Because in line 4, a method that does not exist in class Coin —namely, headsProbability()— is being invoked upon an object referred to by a variable of type Coin. By inspecting the program, we know (based upon the assignment in line 3) that, if this program were allowed to execute, the object to which c is bound would be an instance of BiasedCoin, a class that does have a headsProbability() method, but that does not matter.

To generalize, the rule is this: If x is a (reference) variable (or, more generally, an expression) of type Z, then for the method call x.y() to be legal, y() must be a method either declared in or inherited by class Z.

If line 3 were modified to

BiasedCoin c = new BiasedCoin(0.45);

(which simply changes the data type of c from Coin to BiasedCoin) then the compiler would happily accept this program as being valid, because now the call to headsProbability() in line 4 is being made upon an object that, based solely upon the declared data type of variable c, is necessarily an instance of a class that has the headsProbability() method.


Extension to BiasedCoinWithCounts

As suggested by the BiasedCoinTester1 program, it seems reasonable to expect that clients of the BiasedCoin class might want to determine, after a sequence of coin tosses, how many of them resulted in HEADS and how many in TAILS. For such clients, it would be convenient if, rather than having to keep track of such counts themselves, they could simply "ask" a coin object to provide that information. In other words, (at least some) clients of BiasedCoin would be convenienced if BiasedCoin had methods with these specifications:

Figure 9: Methods to Provide Counts
/* Returns the # of times a toss of this coin resulted in HEADS.
*/
public int headsCount() { ... }

/* Returns the # of times a toss of this coin resulted in TAILS.
*/
public int tailsCount() { ... }

/* Returns the # of times this coin has been tossed.
*/
public int tossCount() { ... }

Of course, we could modify the BiasedCoin class to include such methods, but let's assume that it is "closed for modification". A viable approach, then, would be to extend BiasedCoin, thereby creating a child class thereof (and a grandchild of Coin).

Clearly, it is necessary to introduce instance variables to keep track of how many times HEADS and TAILS were tossed, and, because the values of those variables need to be modified each time a coin is tossed, the toss() method needs to be overridden.

Figure 10: BiasedCoinWithCounts (grandchild of Coin)
public class BiasedCoinWithCounts extends BiasedCoin {

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

   protected int headCntr, tailCntr;

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

   public BiasedCoinWithCounts(double probOfHeads) {
      super(probOfHeads);   // call constructor in parent class
      headCntr = 0;   // not really necessary, as zero
      tailCntr = 0;   // is the default value
   }

   public BiasedCoinWithCounts() {
      super();  // call zero-argument constructor in parent class
      headCntr = 0;   // not really necessary, as zero
      tailCntr = 0;   // is the default value
   }

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

   /* Returns the # of times a toss of this coin resulted in HEADS.
   */
   public int headsCount() { return headCntr; }

   /* Returns the # of times a toss of this coin resulted in TAILS.
   */
   public int tailsCount() { return tailCntr; }

   /* Returns the # of times this coin has been tossed.
   */
   public int tossCount() { return headsCount() + tailsCount(); }

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

   @Override
   public void toss() {
      // invoke parent's version of this method
      super.toss();

      // increment appropriate counter
      if (isHeads()) 
         { headCntr++; }
      else 
         { tailCntr++; }
   }
}

Observe that the new toss() method includes the call super.toss(), which is a call to the parent class's (i.e., superclass's) toss() method. That is, it is a call to the method that is being overridden! This is not unusual, as often what needs to be done by a method in a child class is the same as the corresponding method in the parent class, plus more. Here, the toss() method can carry out a coin toss exactly as its parent class does (justifying the call to the inherited version of the method), but then it must do the extra work of updating either headCntr or tailCntr.

For good measure, here's an example of a client of BiasedCoinWithCounts.

public class BiasedCoinWithCountsTester {

   public static void main(String[] args) {
      BiasedCoinWithCounts c = new BiasedCoinWithCounts(0.35);
      doSomeTosses(c, 100); 
      System.out.printf("# Heads: %d; # Tails: %d\n", 
                         c.headsCount(), c.tailsCount());
   }
   
   /* Tosses the given "coin" the specified # of times;
   */
   private static void doSomeTosses(Coin coin, int numTosses) {
      // Toss the provided coin the specified # of times.
      for (int i=0; i != numTosses; i++) {
         coin.toss();
      }
   }
}


Source Code

Here are source code files of the Java instance classes described above —in some cases, slightly augmented— plus a client application program:

Coin.java   BiasedCoin.java   BiasedCoinWithCounts   CoinTester.java