Class Hierarchies

Copyright © 1997 Kenneth J. Goldman

Let's think over the progression of ideas so far in the course.

Each abstraction technique lets us hide details so we can concentrate on the problem a piece at a time, and gives us a way to look at the "big picture" without drowning in detail.

Now, let's think about what will happen as we try to build larger software systems. We can organize them as collections of objects. But there will be many, many objects! And some of these objects will be similar in their purpose or functionality. We would like abstraction mechanisms to deal with these large collections of objects, to make sense of them in an organized way.

In everyday life or in scientific disciplines, what do people generally do to manage large collections of things? For example, how do biologists think about large numbers of species? How does the Yellow Pages provide an organized way for people to easily find its advertisers? They use:

For example, biologists group animals and plants separately, and each of those categories contain subgroups, etc. They build a classification system of types, subtypes, subsubtypes, etc., as a way of managing large numbers of species.

Similarly, the Yellow Pages has a classification system based on the kinds of goods and services provided by its advertisers.

We can do the same kind of classification for software objects.

In general, each class inherits variables and methods from its superclass, and may add more variables and methods or override existing ones. Here, for example, a ResizableObject has a resize method not available to every GraphicsObject.

Java supports class hierarchy construction using the keyword extends. For example,

public class ResizableObject extends GraphicsObject {

   public void resize(int width, int height) {

      ... // changes object to have the given width and height

   }

}

    

All other methods and all instance variables are inherited. Only the new information that distinguishes this subclass from its superclass needs to be defined. The rest comes "for free" just by saying that the class extends GraphicsObject.

Note the difference between the keywords extends and implements:

A given class may extend at most one other class, but it may (in addition) implement many interfaces. Some languages, such as C++, allow a class to be a subclass of more than one class. This concept, called "multiple inheritance," is not supported by Java.

One of the many benefits of inheritance is the ability to do incremental design and implementation. You can start with some simple classes and then create various subclasses that extend the functionality of the system.

Let's do an example of incremental design using as a starting point the CalorieCounter class we implemented earlier in the course. Recall that the CalorieCounter provided the following methods:

Now suppose we have many happy users of our CalorieCounter, but we want to expand the market. If seems that many potential customers don't like the idea of having to specify the carbs, prot, and fat separately. In fact, all they want to do is tell the calorie counter what food they ate, and let it do the rest.

However, we don't want to upset our existing customer base by changing the CalorieCounter interface. Instead, we'll create a new kind of calorie counter by subclassing the one we already have.

Our subclass needs a new method, eat(Food x), so people can just provide a food object, and the calorie counter will take care of updating the various grams of fat, prots, and carbs.

We'll assume that a Food object has methods to extract grams of fat, proteins, and carbohydrates.

public class HealthOMatic extends CalorieCounter {

   public void eat(Food x) {

      eat(x.getCarbs(), x.getProt(), x.getFat());

   // ^^^ method inherited from the superclass

   }

}

    

That's it! We use the superclass eat method within the new one, to avoid repeating all that effort (checking the limit, etc.). This means that whenever we update the eat method in the parent, the child is automatically updated as well. (The "traditional" eat method is also available.)

Now, to continue with our design, we need to define the Food class.


public class Food {

   int carbs, prot, fat;  // in grams

   String name;



   public Food(int c, int p, int f, String name) {

      carbs = c;

      prot = p;

      fat = f;

      this.name = name;

   }



   public boolean healthy() {

      return (fat * 7 < carbs + prot);

   }



   public int getCarbs() { return carbs; }

   public int getProt () { return prot ; }

   public int getFat  () { return fat  ; }



   public String about() {

      return "food item";

   }



   public String toString() {

      return (name + "(" + carbs + "g carb, " + prot + "g prot, " +

                           fat + "g fat)");

   }

}

    

This is nice. We can create a library of Food objects, and the users of our HealthOMatic just need to pick one whenever they eat.

But maybe we can make it better. There's more to eating than counting calories. We'd like to tap into a high-end market where people are concerned not only about calories, but about balanced diets.

Following the "food pyramid," we can create various kinds of Food objects:

Each subclass could have its own specialized toString method that uses the toString method from the base class (superclass).

public class Meat extends Food {

   public String about() {

      return "meat --- it's what's for dinner";

   }

   public String toString() {

      return "meat " + super.toString();

   }

}

    

Note that super is needed here because there's a local definition of toString in the Meat class and we want to use the one in the parent class. We didn't need to say super.eat in the HealthOMatic example because the compiler could distinguish between the two eat methods in the parent and child classes on that basis of their parameter types.

We could go on adding other methods to the various subclasses of Food, but let's go back to our main discussion. Since we have created all these different kinds of foods, let's see if we can provide a top-of-the-line calorie counter that uses this information.

We can create a subclass of HealthOMatic with even more features. It can keep track of the number of servings from each food group for proper nutritional balance.


public class Dietician extends HealthOMatic {

   int meat, dairy, bread, fruit, veggie, junk;

   // Additional instance variables



   public Dietician(int limit) {

      super(limit);  // calls parent's constructor -- must be first!!

      meat = dairy = bread = fruit = veggie = junk = 0;

   }



   public void reset() {

      meat = dairy = bread = fruit = veggie = junk = 0;

      super.reset();

   }



   public void eat(Food f) {

      if (! f.healthy())

         junk++;

      else if (f instanceof Meat)

         meat++;

      else if (f instanceof Fruit)

         fruit++;

      else if ...

         ...

      super.eat(f);

   }



   public void cheat(int fat) {

      Terminal.println("Get with the program!");

   }



   public String toString() {

      return (string containing info about food groups);

   }

}

    

instanceof is a built-in operator to see if an object is an instance of a class.

We could also add new methods for accessing food group information, and add other features, but this is sufficient to illustrate the concept of incremental design.

Technical Issues Surrounding the Class Hierarchy Concept

Some things to remember:

Polymorphism

We have seen that one of the advantages of inheritance is the ability to quickly define new types that are specializations of types we have previously defined. For example, we defined the Food class, and then several specializations, such as the Meat and Fruit classes.

We have also seen that a class hierarchy represents "is-a" relationships. Each instance of a subclass is also an instance of the parent class and all ancestors. For example, Meat "is-a" Food and Food "is-a" Object. This meant that we could create a calorie counter that could receive any Food object as a parameter to its eat method, but that we could actually pass instances of Food or any of its subclasses into the eat method.

The beauty of this is that when we have an instance of Food, we can call any of the methods defined for Food. If the object happens to be an instance of a subclass of Food that has overriden the method, then we'll automatically get the specialized behavior defined for that subclass. That is, when we call the method, we need not know what kind of Food we have. We just call the method and the object knows how to perform whatever action is defined by its particular specialized subclass.

This powerful idea, of invoking methods on objects that may do different things depending on their specialization, is called polymorphism. The roots are poly, meaning "many," and morph, meaning "shape." We deal with a general class of object that may be instantiated with many "shapes", defined by specialized subclasses, but we treat them uniformly, as instances of the more general class.

Let's go back to the graphics object hierarchy and see how the power of polymorphism is used.

Several different things happen in a class hierarchy:

Polymorphism exploits the notion of specialization. This is most clearly seen in the implementation of the CS101Canvas. The canvas contains, as part of its internal representation, a linked list of GraphicsObject's.

Note that although each GraphicsObject is different (lines, text, ovals, etc.), the canvas doesn't "know" that they are different. It treats them all uniformly, as graphics objects. Whenever the add method is called, the canvas puts the GraphicsObject in the list.

Then how do the graphics objects show up differently on the screen? That's the beauty of polymorphism...

The canvas has a repaint method that is called whenever something changes to require that the graphics be redrawn on the screen. (For example, when an object is moved or when an overlapping window uncovers part of the canvas.) The repaint method goes through the list, something like this:


GraphicsList ptr = first;

while (ptr != null) {

   ptr.graphic.draw();

   ptr = ptr.next;

}

    

Note that ptr.graphic has type GraphicsObject. The canvas doesn't "know" the specific type of GraphicsObject, though. It just calls the draw method on each one. And, since each subclass (Line, Oval, Rect, Text, etc.) has defined its own specialized draw method, each of the graphics objects "knows" how to draw itself.

There are no special cases, no big if-then-else statements. Just a simple call to draw is all it takes, and each object knows what to do.

This is only one example of what polymorphism can do.
Kenneth J. Goldman
Last modified: Mon Apr 21 01:36:22 CDT 1997