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
- Compile and run Listing 8 Then add a fourth class
Poodlethat extendsDogand adds a methodgroom. Instantiate aPoodleand verify it can calleat,breathe,bark, andgroom. The chapter marksDogasfinal— what change must you make first and why doesfinalprevent this? - The chapter shows that
DoginheritseatfromAnimalwithout redefining it. Create anAnimal[]containing one instance each ofAnimal,Mammal, andDogand calleaton each through the array. What prints for each? What does this demonstrate about how inherited methods behave when called through a parent type reference? - The chapter introduces
superto call parent constructors. Remove thesuper(n)call fromMammal's constructor and attempt to compile. What error do you get? Now add a no-argument constructor toAnimalthat setsnameto"Unknown"and recompile withoutsuper. What value doesnamehave? What does this tell you about whensuperis required? - Change the
namefield inAnimalfrom default (package-private) access topublicand recompile. Does anything break? Now change it toprotectedand recompile. What is the practical difference betweenpublicandprotectedfor an inherited field? Which would you choose and why? - The chapter states that
instanceofchecks the entire inheritance chain. Write a program that creates one instance each ofAnimal,Mammal, andDogand tests every combination ofinstanceof(e.g.dog instanceof Animal,animal instanceof Dog, etc.). Print each result, predict it before running, and explain any results that surprise you. - Rewrite the
Animal,Mammal, andDoghierarchy replacing inheritance with composition:Mammalshould contain anAnimalfield rather than extending it, andDogshould contain aMammalfield. Implementeat,breathe, andbarkby 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? - The chapter marks
Dogasfinal. Look up Java'sMathclass andStringclass — are they alsofinal? Why might the designers of Java have made those classesfinal? Give one reason why marking a classfinalis a deliberate design decision rather than just a restriction. - Add a
Catclass that also extendsMammaland has ameowmethod. Then create anAnimal[]containing aDogand aCatand use an enhanced for loop withinstanceofto call the appropriate sound method (barkforDog,meowforCat) on each element. What does this exercise demonstrate about the relationship between inheritance and polymorphism? - The chapter shows that
Mammalcallssuper(n)to pass the name up toAnimal. Add a second fieldint agetoAnimaland update its constructor to accept bothnameandage. Trace the changes that must be made down throughMammalandDogto keep everything compiling. What does this exercise reveal about the fragile base class problem? - The chapter shows that Java uses the
extendskeyword 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.