Beej's Bit Bucket

 ⚡ Tech and Programming Fun

Object-Oriented Thinking Part Two


(See "Object-Oriented Thinking, Part One", if you haven't done so already.)

In an earlier article, I talked a bit about how to get started with Object Oriented Programming (OOP), and I presented some information about encapsulation.

But this time we're going to go a step further and talk about another major cornerstone of OOP: inheritance.

A sample class hierarchy. In real life, the Animal class might be subclassed into Mammals, Reptiles, etc. Don't under-engineer it, but don't over-engineer it, either.

Now bear with me while I get all theoretical for a couple paragraphs.

With a simple object, we wrap up all the object's attributes and methods in a class and use them as we see fit. But we can get a bit more power out of our code by organizing the object types into what is known as a class hierarchy.  Basically, the root of the hierarchy contains methods and attributes shared by all the classes below it (which, since it's the root, is all the classes.) For each class in the hierarchy, all classes below it automatically share the attributes and methods of the classes above it.

Imagine you have a class X and it is the parent of a class Y. This means that Y automatically inherits all of X's attributes and methods.  X is called the parent class, or the superclass, or the base class. Y is called the derived class, or the subclass, or the child class.

Class hierarchies can be gigantic. Here's a relatively sane one from the I/O Java package.

A concrete example of a parent class might be "Animal", and some child classes might then be "Rabbit", "Armadillo", and "Salamander". The "Animal" base class would only contain things that were common to all animals, such as "canSwim" or "legCount", whereas the specific animal classes would specialize on it, and include things that only had particular meaning for particular animals; Armadillo might include "armorRating", for example, while Salamander might have "moistness".

Ok, the time for theory is ending. What we want to know is, in practical terms, how do we set up a parent-child relationship between a superclass and subclass?

Naturally, the answer depends entirely on the language, and the syntax is different between all of them.

Let's set up a base class in Java for our animals:

class Animal { public int legCount; public boolean hasFeathers; /** * Constructor */ public Animal(int legCount, boolean hasFeathers) { this.legCount = legCount; this.hasFeathers = hasFeathers; } /** * Eat some food */ public void eat() { System.out.println("Animal is eating."); } }

So what we have there is an animal with some features that apply to all animals, namely legCount and hasFeathers. And we have an action that all animals do, which is eat().

Why would you bother breaking out this functionality into a superclass in the first place? Well, what it means is that you have one piece of code that controls this common functionality, so the whole project is more maintainable. If you have 30 animals and they all use the Animal superclass, fixing a bug in Animal fixes it in all the subclasses.

Also, it means that each animal doesn't need to reimplement all the same code.

And, very importantly it means that other modules in the software can work with the base class Animal, and so those modules won't need modification if you add a new animal subclass later.

And what we can do now is make a subclass of this superclass. We'll make an Armadillo, because I like those:

class Armadillo extends Animal { public int armorRating; /** * Constructor */ public Armadillo(int armorRating) { super(4, false); // 4 legs, no feathers this.armorRating = armorRating; } }

There! There on the first line! That's where we set up the class hierarchy in this example! By saying Armadillo extends Animal, we're telling Java that Animal is the base class, and Armadillo inherits from it, and is its child class. We're saying that "Everything Animal has, Armadillo also has. Plus Armadillo has these other armadillo-specific things, too."

I've highlighted the keyword "super" in there, too. This is a pseudo-function that means, "Call the superclass constructor with these parameters." In this case, we want to say that an armadillo has four legs and no feathers, so we do that.

Let's go ahead and build a main() out and instantiate an Armadillo and give it a spin:

class Main { public static void main(String args[]) { Armadillo aaron = new Armadillo(12); // armor rating 12 System.out.println("aaron: leg count: " + aaron.legCount); System.out.println("aaron: has feathers: " + aaron.hasFeathers); System.out.println("aaron: armor rating: " + aaron.armorRating);; } }

And when we run this, we get the following output:

aaron: leg count: 4
aaron: has feathers: false
aaron: armor rating: 12
Animal is eating.

Exercise: make a class for birds that inherits from the Animal base class. What specializations might you put in that are specific to birds?

Now the keen observers among you might have noticed that Java's super() pseudo-function calls the one and only superclass's constructor. There's no way in Java to inherit from multiple base classes, an idea known as multiple inheritance. Some languages, including Java, shun multiple inheritance, an instead use an idea called interfaces to achieve a similar effect. Other languages like Python and C++ support multiple inheritance. But this is something for another time.

Now we're going to switch gears just a tad, and talk about one more important idea: polymorphism. This is the concept that allows subclasses to use base class functionality if they want, or they can override this functionality and implement their own.

Notice that when the Armadillo instance aaron eats, it prints out "Animal is eating", and that's all. What if we want to something different just for Armadillos? Let's override the eat() method and have it print a different message and increase our armor rating (because we're making a healthier armadillo!) Here's a new version of the Armadillo class that does this:

class Armadillo extends Animal { public int armorRating; public Armadillo(int armorRating) { super(4, false); // 4 legs, no feathers this.armorRating = armorRating; } /** * Override base class's eat() method */ public void eat() { System.out.println("Armadillo is eating and getting more armor!"); this.armorRating++; } }

See how merely by naming the method the same thing as in the base class, we've overridden it, and now when aaron() calls his eat() method, it will use his version instead:

aaron: leg count: 4
aaron: has feathers: false
aaron: armor rating: 12
Armadillo is eating and getting more armor!

That's all good, right? Pretty straightforward. Now it's time to blow your mind. Ready?

When a subclass has overridden a method like this, that new method is the one that's used even if you're looking at the instance as its base class. What does that mean? Well, we say that Armadillo is a Animal, because it is derived from the base class Animal. An armadillo is an animal! We can actually store a reference to Armadillo in an Animal variable, but it's still really an Armadillo deep down.

In the following code, we make an array of Animals, but then we initialize some of them as the base class Animal, and others of the derived class Armadillo. Remember, it's possible to store an Armadillo reference in an Animal variable, because an Armadillo is a Animal:

class Main { public static void main(String args[]) { Animal[] animalList = new Animal[5]; // array of 5 Animals animalList[0] = new Animal(17, true); // 17 legs, has feathers animalList[1] = new Armadillo(30); // armor rating 30 animalList[2] = new Animal(27, false); // 27 legs, no feathers animalList[3] = new Armadillo(20); // armor rating 20 animalList[4] = new Armadillo(10); // armor rating 10 for (int i = 0; i < animalList.length; i++) { animalList[i].eat(); } } }

And then we call the eat() method of all the Animals in the animalList. What happens? Will it print out "Animal is eating" five times? Or will it call the specific eat() method for Animals and Armadillos? Let's try it:

Animal is eating.
Armadillo is eating and getting more armor!
Animal is eating.
Armadillo is eating and getting more armor!
Armadillo is eating and getting more armor!

Check it out! Even though the array is of type Animal, it knows (by computer science magic) to call the eat() method specific to the type actually stored!

Methods that can be overridden are said to be virtual. In Java, all methods are virtual unless you declare them with the final keyword.  In C++, none of the methods are virtual unless they are declared with the virtual keyword.

It always helps me to think about OOP in terms of games, because they tend to have things in them that map well to objects. So imagine you have a game world that's full of different types of Animals, and each round of the game, all the animals have to move(). But maybe the move() method in the Animal base class doesn't actually do anything, and just leaves the animal in the same place. So each subclass, like Armadillo, Hawk, or Whale, overrides the move() method so that it can move in its own particular and peculiar way. Finally, the "animal manager" object, which has a big array of Animals, just loops through the array and calls move(), just like the example above called eat(). And each animal does it's special move!

Share me!

Historic Comments

 Xerion 2010-02-02 20:57:58

Since my enlightenment to OOP I've used inheritance a great deal in my projects. One thing that I heard somewhere, was that there is a performance loss for using virtual functions. Have any thoughts about this?

 beej 2010-02-02 21:12:27

@Xerion Yes, there certainly is a loss, since it's an indirect jump rather than a direct one. (That is, you need to look up in a table which function to jump to, because the instance might actually be a derived class that has overridden one of the base class's methods.)

Even though a memory lookup (and possible cache miss) is something to consider, in this modern day of really fast machines, it's less and less of a practical issue, and most people just ignore it. (I mean, if you're running Java, you're already not at your peak efficiency compared to, say, assembly language, and that's okay!)

But if utmost speed is of the essence in some inner loop or something, you might not want to call a virtual function.


blog comments powered by Disqus
Blog  ⚡  Email  ⚡  Home page