2010-01-22
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.
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.
But this time we're going to go a step further and talk about another major cornerstone of OOP: inheritance.
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 inAnimal
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);
aaron.eat();
}
}
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 Armadillo
s? 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 Animal
s, 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 Animal
s in the
animalList
. What happens? Will it print out "Animal is eating" five
times? Or will it call the specific eat()
method for Animal
s and
Armadillo
s? 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 thevirtual
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 Animal
s, 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 Animal
s, just loops
through the array and calls move()
, just like the example above called
eat()
. And each animal does it's special move!
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?
@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.