| Home | Resources | Papers | Essays | Short Fiction | About |

Inheritance

Table of Contents

Introduction

When introducing objects, I discussed the idea that they are powerful tools of abstraction which allow for us to reprsent real-world things in the form of user-defined types that are subsequently managed by the Java runtime system. I had emphasized that there is a difference between a t-shirt-as-a-concept and a particular t-shirt-as-an-object. I had also mentioned that t-shirts can be abstracted away as clothing—a t-shirt is an article of clothing (it is made of some sort of fabric and it is worn to cover the body). Object-oriented programming allows us to construct types that are in relation to one another, that form a hierarchy. In Java, we call this hierarchy "inheritance".

What is Inheritance?

In Object-oriented programming inheritance refers to the concept of constructing objects which take on qualities of their predecessors. Recall what you know of basic biology. There are a variety of classification strata which denote where certain living organism reside in relation to one another. Let's take the humble dog or canis lupus familiaris as an example. A dog sits under a variety of hierarchical classifications:

Living Thing
└── Animal
    ├── Arthropod
    ├── Mollusk
    └── Chordate
        ├── Fish
        ├── Reptile
        ├── Bird
        └── Mammal
            ├── Rodent
            ├── Primate
            └── Carnivore
                ├── Cat
                ├── Bear
                └── Dog ← (Canis lupus familiaris)

A dog is a living thing, an animal, a mammal, and a carnivore. Within each grouping provides more specific information about the bodily makeup of the organism and the sorts of behaviors that it might express. A mammal breathes air, a carnivore eats meat, a chordate has a spinal chord (or a spinal chord-like structure), etc.

Java allows for us to construct similar groupings through the use of class hierarchies. For example I can create an Animal class:

1: class Animal{
2:     String name;
3:     Animal(String n){
4:         name = new String(n);
5:     }
6:     void eat(){
7:         System.out.printf("%s eats!%n", name);
8:     }
9: }

I can then create a subclass of Animal called Mammal:

1: class Mammal extends Animal{
2:     Mammal(String n){
3:         super(n);
4:     }
5:     void breathe(){
6:         System.out.printf("%s breathes!%n", name);
7:     }
8: }

The extends keyword is what turns Mammal into a subclass of Animal. We say that Animal is the "superclass" of Mammal and Mammal is a subclass of Animal. It so happens that Java uses "single-inheritance" where a class may only extend a single class1. Furthermore, in the constructor on Line 4 of Listing 2 we see a new keyword, super. super explicitly references members of the super class—when in the context of the constructor it can be used as a method call to the "super constructor" (the constructor of the superclass).

How Inheritance Works

Although it may seem strange to conceptualize at first, the basics of inheritance are actually reasonably intuitive. A subclass will inherit all members (fields and methods) of the superclass. For example, Mammal gets name and eat() from Animal:

1: public class Farm{
2:     public static void main(String[] args){
3:         Mammal m = new Mammal("Lassie");
4:         m.eat();
5:         m.breathe();
6:     }
7: }

After compiling and running we get:

Lassie breathes!josephraskind@stargazer:/tmp/inheritance$ java Farm.java 
Lassie eats!
Lassie breathes!

We can see that even though m is only declared as a Mammal type we get the original eat() from Animal alongside the breathe() from Mammal! Again, this is because Mammal is a subclass of Animal and therefore inherits all of what Animal has available.

It is important to understand this does not work in the opposite direction. For example:

1: public class Farm{
2:     public static void main(String[] args){
3:         Animal a = new Animal("Lassie");
4:         a.eat();
5:         a.breathe();
6:     }
7: }

After compiling the above we get:

josephraskind@stargazer:/tmp/inheritance$ javac Farm.java 
Farm.java:5: error: cannot find symbol
        a.breathe();
         ^
  symbol:   method breathe()
  location: variable a of type Animal
1 error
error: compilation failed

We can see that Farm was incapable of compiling because the symbol breathe could not be found. This is because Animal does not have access to breathe as it is defined within a subclass---Animal has no understanding of its subclasses!

Subsubclasses

We can create arbitrarily many subclasses by adding more classes to the hierarchy chain:

1: class Dog extends Mammal{
2:     Dog(String n){
3:         super(n);
4:     }
5:     void bark(){
6:         System.out.printf("%s barks!%n", name);
7:     }
8: }

Dog's superclass is Mammal whose superclass is Animal therefore Dog can reference all three methods:

1: public class Farm{
2:     public static void main(String[] args){
3:         Dog d = new Dog("Lassie");
4:         d.eat();
5:         d.breathe();
6:         d.bark();
7:     }
8: }

After compiling and running we get:

josephraskind@stargazer:/tmp/inheritance$ java Farm.java 
Lassie eats!
Lassie breathes!
Lassie barks!

Variable Shadowing

Occassionally it may make sense to redifine fields within subclasses that share the names of fields in superclasses. The way that Java handles these identical symbol sets is through the use of variable shadowing. This means that the execution context informs the symbol that is being referenced. For example, if I alter Animal and Mammal to both have a weight field like so:

 1: class Animal{
 2:     String name;
 3:     double weight;
 4:     Animal(String n, double w){
 5:         name = new String(n);
 6:         weight = w;
 7:     }
 8:     void animalWeight(){
 9:         System.out.printf("%s weighs %.2fkg!%n", name, weight);
10:     }
11: }
12: class Mammal extends Animal{
13:     double weight;
14:     Mammal(String n, double w){
15:         super(n, w);
16:     }
17:     void mammalWeight(){
18:         System.out.printf("%s weighs %.2fkg!%n", name, weight);
19:     }
20: }

There are now two separate weight fields: one for Animal and one for Mammal. We can test this out with the following:

1: public class Farm{
2:     public static void main(String[] args){
3:         Mammal m = new Mammal("Lassie", 21.5);
4:         m.weight = 18.5;
5:         m.animalWeight();
6:         m.mammalWeight();
7:     }
8: }

Compiling and running the above we get:

josephraskind@stargazer:/tmp/inheritance$ java Farm.java 
Lassie weighs 21.50kg!
Lassie weighs 18.50kg!

How can this be?! Lassie can't weight 21.5kg and 18.5kg at the same time! The answer has to do with shadowed variables. There are actually two weight fields contained within the Mammal object at the same time: one tied to the Animal context and the other tied to the Mammal context. The Java compiler does nothing to advise us on this fact because it takes for granted that we are aware of the possibility of variable shadowing. You should always be careful when shadowing variables!

Access Modifiers

We learned previously that we can tag access modifiers onto members of a class (as well as the classes themselves). Doing so restricts the visibility of those members in regards to other classes. Naturally, we can also use access modifiers within inherited classes as well. For example, I can set the name field to private in Animal:

1: class Animal{
2:     private String name;
3:     Animal(String n){
4:         name = new String(n);
5:     }
6:     void eat(){
7:         System.out.printf("%s eats!%n", name);
8:     }
9: }

If we assume the default implementation of Mammal in Listing 2 and try to compile the code in Listing 3 we get:

josephraskind@stargazer:/tmp/inheritance$ java Farm.java 
/tmp/inheritance/Mammal.java:6: error: name has private access in Animal
        System.out.printf("%s breathes!%n", name);
                                            ^
1 error
error: compilation failed

Even though name is inherited by Mammal it cannot access it because it has been set to private within Animal.

The is-a Relationship

In OOP, inheritance establishes relationships between different classes. We can categorize these relationships into two camps the "is-a" and the "has-a". Let's take a look at the Dog hierarchy:

                  Animal
                +--------+
                | name   |  ← has-a String
                +--------+
                | eat()  |
                +--------+
                     △
                     |  is-a (extends)
                     |
                  Mammal
                +----------+
                | breathe()|
                +----------+
                     △
                     |  is-a (extends)
                     |
                    Dog
                +--------+
                | bark() |
                +--------+

is-a relationships:
  Dog    is-a Mammal
  Dog    is-a Animal
  Mammal is-a Animal

has-a relationships:
  Animal has-a String (name)
  Mammal has-a String (name)  ← inherited
  Dog    has-a String (name)  ← inherited

We can verify "is-a" relationships through the use of the instanceof operator:

 1: public class Farm{
 2: 
 3:     public static void checkInstance(Animal a){
 4:         System.out.printf("%s instanceOf Animal: %b%n",
 5:                            a.name,
 6:                            a instanceof Animal);
 7:         System.out.printf("%s instanceOf Mammal: %b%n",
 8:                            a.name,
 9:                            a instanceof Mammal);
10:         System.out.printf("%s instanceOf Dog: %b%n",
11:                            a.name,
12:                            a instanceof Dog);
13:     }
14: 
15:     public static void main(String[] args){
16:         Animal a = new Animal("Alfred");
17:         Mammal m = new Mammal("Margery");
18:         Dog d    = new Dog("Douglas");
19: 
20:         checkInstance(a);
21:         checkInstance(m);
22:         checkInstance(d);
23:     }
24: }

After compiling and running we get:

josephraskind@stargazer:/tmp/inheritance$ java Farm.java 
Alfred instanceOf Animal: true
Alfred instanceOf Mammal: false
Alfred instanceOf Dog: false
Margery instanceOf Animal: true
Margery instanceOf Mammal: true
Margery instanceOf Dog: false
Douglas instanceOf Animal: true
Douglas instanceOf Mammal: true
Douglas instanceOf Dog: true

Line 3 in Listing 15 might be a bit confusing, but the next chapter on polymorphism will explain how we can send in a Dog argument to an Animal parameter.

Why Inheritance?

We have seen that the advantage of inheritance comes in the form of passing down all of the members associated with a superclass, the superclass's superclass, and so on. But why is this advantageous? The most obvious benefit comes in the form of code reuse. When we create Mammal we do not need to rewrite eat() because we get it for free as Mammal is a subclass of Animal. As a corollary, if we want to change eat() we only have to change it in Animal and the change will be represented in Mammal, Dog, and any other class which extends Animal. We also get to leverage a powerful type system which can do things like runtime and static typechecking to make sure that certain behaviors are capable of being performed during the execution of our programs. When we write a program that uses a Dog object we know that a Dog should have a name and should be able to invoke the functions eat(), breathe(), and bark().

Why Not Inheritance?

OOP is not without its faults and inheritance is something that is hotly contested as being a benefit to a language. Although code reuse can save time it can also cause massive problems. For example, if a superclass is changed in such a way that a new bug is introduced, all subclasses will now inherit that bug through no fault of their own! Many also find the "is-a" relationship too constricting as it locks a class down into a rigid hierarchy—classical solutions involve the championing of "has-a" relationships over "is-a" relationships. Encapsulation is also often violated through inheritance as subclasses will frequently demand total knowledge of their superclass ancestors.

Preventing Subclasses

Because of the pitfalls associated with inheritance, Java includes the option for programmers to prevent subclassing of certain classes. This can be achieved by tagging a class definition with final:

1: final class Dog extends Mammal{
2:     Dog(String n){
3:         super(n);
4:     }
5:     void bark(){
6:         System.out.printf("%s barks!%n", name);
7:     }
8: }

Now if I try to extend Dog:

1: class Collie extends Dog{
2:     Collie(String n){
3:         super(n);
4:     }
5:     void breed(){
6:         System.out.printf("%s is a Collie!%n", name);
7:     }
8: }

and I compile the above I get the following error:

josephraskind@stargazer:/tmp/inheritance$ javac Collie.java 
Collie.java:1: error: cannot inherit from final Dog
class Collie extends Dog{
                     ^
1 error

The final keyword makes it so Dog can never have any subclasses.

Exercises

  1. Compile and run Listing 8 Then add a fourth class Poodle that extends Dog and adds a method groom. Instantiate a Poodle and verify it can call eat, breathe, bark, and groom. The chapter marks Dog as final — what change must you make first and why does final prevent this?
  2. The chapter shows that Dog inherits eat from Animal without redefining it. Create an Animal[] containing one instance each of Animal, Mammal, and Dog and call eat on each through the array. What prints for each? What does this demonstrate about how inherited methods behave when called through a parent type reference?
  3. The chapter introduces super to call parent constructors. Remove the super(n) call from Mammal's constructor and attempt to compile. What error do you get? Now add a no-argument constructor to Animal that sets name to "Unknown" and recompile without super. What value does name have? What does this tell you about when super is required?
  4. Change the name field in Animal from default (package-private) access to public and recompile. Does anything break? Now change it to protected and recompile. What is the practical difference between public and protected for an inherited field? Which would you choose and why?
  5. The chapter states that instanceof checks the entire inheritance chain. Write a program that creates one instance each of Animal, Mammal, and Dog and tests every combination of instanceof (e.g. dog instanceof Animal, animal instanceof Dog, etc.). Print each result, predict it before running, and explain any results that surprise you.
  6. Rewrite the Animal, Mammal, and Dog hierarchy replacing inheritance with composition: Mammal should contain an Animal field rather than extending it, and Dog should contain a Mammal field. Implement eat, breathe, and bark by delegating to the contained objects. Compare the two approaches — what do you gain and what do you lose by using composition instead of inheritance here?
  7. The chapter marks Dog as final. Look up Java's Math class and String class — are they also final? Why might the designers of Java have made those classes final? Give one reason why marking a class final is a deliberate design decision rather than just a restriction.
  8. Add a Cat class that also extends Mammal and has a meow method. Then create an Animal[] containing a Dog and a Cat and use an enhanced for loop with instanceof to call the appropriate sound method (bark for Dog, meow for Cat) on each element. What does this exercise demonstrate about the relationship between inheritance and polymorphism?
  9. The chapter shows that Mammal calls super(n) to pass the name up to Animal. Add a second field int age to Animal and update its constructor to accept both name and age. Trace the changes that must be made down through Mammal and Dog to keep everything compiling. What does this exercise reveal about the fragile base class problem?
  10. The chapter shows that Java uses the extends keyword to create a hierarchy of classes. Using only what you have learned so far, design a three-level class hierarchy of your own choosing that is different from the animal example. Write out the classes with appropriate fields and methods at each level, ensuring each subclass adds something new. Instantiate one object of each class and verify that the bottom-level class has access to all methods defined at every level above it.

Footnotes:

1

Other OOP languages like C++ and Python allow for multiple inheritance. Java eschewed its inclusion due to complications that can arise in highly complex class dependency hierarchies—often referred to as the diamond problem.

Contact: [email protected] | rss feed | Compiled with org-mode | Licensed under CC BY-NC-SA 4.0