An Explanation of Inheritance

Here is an explanation of inheritance based on real world examples. Many thanks to Stuart Reges of the University of Arizona for providing this.

Consider the following code example

public class D extends B implements I {...}

D and B are concrete classes and each has a no-args constructor.

Which is not syntactically correct?

     Object obj = new B();
     B obj = new D();
     D obj = new B();
     D obj = new D();

First of all, the terms "superclass" and "subclass" are unfortunate choices because they have the opposite meaning to what we normally use "super" and "sub" to mean. In OOP, subclasses are more sophisticated, more specialized and have more capabilities. I usually make a joke that if you had an inheritance hierarchy for burgers you'd make "plain burger" the superclass and cheeseburger the subclass. So someone might think to themselves, "I love cheeseburgers, so give me a super cheeseburger." Unfortunately, the superclass of cheeseburger doesn't have any cheese at all.

Try to use terms like "base class" and "derived class" because the word "base" implies that it is more primitive than the derived class. At least Java uses the keyword "extends" for inheritance, which strongly implies the fact that the subclass is bigger than the original. In C# you use the keyword "base" instead of the keyword "super."

For example, I might have a hierarchy with Employee at the top and below that "Clerical" and "Professional." Under "Professional" I might put lawyer and software engineer. Under clerical I might put secretary. And under secretary I might put legal secretary.

Think about the "role" of each of the positions listed above. Each entry in the hierarchy represents a different role. The basic rule of the hierarchy is that as you move up the hierarchy, you have to be able to fulfill all of the roles (this is the "isA" relationship). So consider my legal secretary. It is under secretary, which is under clerical which is under employee. That means that a legal secretary has to be able to fulfill all of these roles. A legal secretary has to be able to do the job of a legal secretary and the job of a regular secretary and the job of a generic clerical person and the job of a generic employee. It doesn't work going in the other direction. Not every clerical person will be able to do the job of a secretary. Not every secretary will be able to do the job of a legal secretary. And you also can't go across. We don't expect a legal secretary to be able to do the job of a lawyer (which was in the other branch deriving from employee).

This analogy alone answers your question below. You have a class D that extends B. That means that a D object has to be able to take on both the D role and the B role. But the opposite is not true. 

This is exactly like the secretary vs the legal secretary (D is the sophisticated role, because it extends B). 

So it's okay to say: 

B obj = new D(); 
D obj = new D();

That's a D object filling the B role in the first case and the D role in the second case. 

But it's not okay to say: 

D obj = new B();

A B object can't fill the D role (not all secretaries are legal secretaries).

This way of thinking also helps you to explain when casting is necessary. I go back to my employee analogy. Suppose you run a temp agency and you charge $10/hour for a secretary and $15/hour for a legal secretary. Someone calls you and asks you to send over a secretary. You don't have any secretaries left, but you have a legal secretary you can send over. You're sending over someone who is overqualified for the job, but it might be worth it to you to have this guy working even if you're only getting $10/hour for his work instead of the normal $15/hour. So the legal secretary goes over and starts doing some secretary work, but somehow they figure out that this guy is really a legal secretary and they say, "Fabulous! We have some legal work we'd also like to get done."

What's wrong with this scenario? The company hiring this worker contracted for a secretary and that's why they're paying $10/hour. The temp agency happened to send over someone who could do more, but the contract is for a regular secretary. If they want to have that person doing the work of a legal secretary, they have to renegotiate the contract and pay $15/hour for the work. 

That's an exact parallel to what you're doing when you cast. 

If you say: 

B obj = new D();

you're constructing an object that can do the job of either a B or a D. The variable type on the left specifies the contract. You have contracted for an object that can do the B role. 

Suppose that the D role includes a method called 

somethingFancy() 

that isn't in the B role. D is an extension of B, so naturally it can have extra methods. 

So what happens if you try to call: 

obj.somethingFancy();

You get a compiler error. Even though you have an object that can perform that method, you get a compiler error. Why? Because of the contract. The contract was for the B role and the B role doesn't include the somethingFancy method. 

So you have to renegotiate the contract by saying: 

((D)obj).somethingFancy();

You're saying to the compiler, "I know that when I defined this I asked for the B role, but trust me, this object is really more sophisticated than that. It can handle the D role as well." The compiler then checks to make sure that the D role includes the somethingFancy method and if it does, then this is okay. Of course, there is still a runtime check because Java doesn't want to rely on someone saying "trust me." So at runtime Java will make sure that the actual object can do the D role and if not, you get an illegal cast exception.

Your example also included an interface. Those are just more roles. Objects that implement the Comparable interface, for example, have an extra role they can fill in addition to the roles that come from their inheritance hierarchy.

Let me give one quick last note about this. In Java all classes are derived from Object one way or another. That means that every object in Java can fill the Object role. That's why we define collections classes like stacks, queues, lists and hashtables to take values of type Object. That way anything can go into these collections. But that means that we have to define methods that return values from these collections as returning values of type Object. We realize that the actual objects are going to be something more sophisticated than a simple object, but to write this code generically, we have to fall back on Object. So then you have to say to yourself, "I know that I put objects of type String into this collection. So when they come back out I'll have to cast to String to let Java know that they really are strings."

By the way, a lot of this casting stuff will go away when we have generics in Java. They are supposed to be included in the next major release (JDK1.5) along with a lot of other features that look a lot like C#. People who remember templated classes in C++ will see the parallel when Java adds generics and we can define collections that are of type "Stack<String>" instead of just "Stack."

My employee analogy also provides a nice explanation for the idea of abstract classes. Notice that everything derives from Employee, so everyone who works for the company is an employee. But can you imagine someone actually describing themselves as having a career as an employee? "What do you do for the company?" "I'm an employee." "I know you're an employee, but what do you DO for the company?" "I'm an employee. That's it. Nothing more." We all know that doesn't happen. Nobody is JUST an employee even though they all are employees. Employee is a great example of something you'd make into an abstract class. It has an important function to serve in defining this hierarchy and the relationships between the real (what will become concrete) values in the hierarchy. But you would never want someone to say: Employee x = new Employee();You'd force them to construct objects of a more specific type (a secretary or legal secretary, for example).

--Stuart Reges