Inheritance in Java

· 29 mins read

Among the most important concepts in object oriented programming are the concepts of inheritance and polymorphism. Class inheritance is the mechanism whereby a class acquires (inherits) the methods and variables of its superclasses. Let’s consider an example of a natural form of inheritance: Just as horses inherit the attributes and behaviors associated with mammals and vertebrates, a Java subclass inherits the attributes and behaviors of its superclasses.

Natural example explaining the inheritance concept

As shown in the diagram above, the root of the hierarchy, which is always shown at the top, the Animal class contains the most general attributes, such as being alive and being able to move. All animals share these attributes. The class of vertebrates is a somewhat more specialized type of animal, in that vertebrates have backbones. Similarly, the class of mammals is a further specialization over the vertebrates in that mammals are warm-blooded and nurse their young. Finally, the class of horses is a further specialization over the class of mammals, in that all horses have four legs. Some mammals, such as humans and penguins, do not have four legs. Thus, by virtue of its class’s position in this hierarchy, we can infer that a horse is a living, moving, four-legged vertebrate, which is warm blooded and nurses its young.

We have deliberately used an example from the natural world to show that the concept of inheritance in Java is inspired by its counterpart in the natural world. But how exactly does the concept of inheritance apply to Java (and to other object-oriented languages)? And, more importantly, how do we use the inheritance mechanism in object-oriented design?

  1. Using an Inherited Method
  2. Overriding an Inherited Method
  3. Static Binding and Dynamic Binding
  4. Polymorphism
  5. Polymorphism and Object-Oriented Design
  6. Using super to Refer to the Superclass
  7. Inheritance and Constructors
  8. Abstract Classes, Interfaces, and Polymorphism
    1. Implementing an Abstract Method
    2. Implementing a Java Interface

Using an Inherited Method

In Java, the public and protected instance methods and instance variables of a superclass are inherited by all of its subclasses. This means that objects belonging to the subclasses can use the inherited variables and methods as their own.

For example, all Java classes are subclasses of the Object class, which is the most general class in Java’s class hierarchy. One public method that is defined in the Object class is the toString() method. Because every class in the Java hierarchy is a subclass of Object, every class inherits the toString() method. Therefore, toString() can be used with any Java object.

Suppose we define a Student class as follows:

public class Student {
    protected String name;
    public Student(String s) {
        name = s;
    }
    public String getName() {
        return name;
    }
}

The Student class hierarchy.

The figure above shows the relationship between this class and the Object class. As a subclass of Object, the Student class inherits the toString() method. Therefore, for a given Student object, we can call its toString() as follows:

Student stu = new Student("Stu");
System.out.println(stu.toString());

How does this work? That is, how does Java know where to find the toString() method, which, after all, is not defined in the Student class? The answer to this question is crucial to understanding how Java’s inheritance mechanism works.

Note in this example that the variable stu is declared to be of type Student and is assigned an instance of the Student class. When the expression stu.toString() is executed, Java will first look in the Student class for a definition of the toString() method. Not finding one there, it will then search up the Student class hierarchy (Fig. 8.2) until it finds a public or protected definition of the toString() method. In this case, it finds a toString() method in the Object class and it executes that implementation of toString().

Overriding an Inherited Method

The default implementation of toString() returns the name of the object’s class and the address (e.g. cde100) where the object is stored in memory. However, this type of result is much too general and not particularly useful.

The toString() method is designed to be overridden—that is, to be redefined in subclasses of Object. Overriding toString() in a subclass provides a customized string representation of the objects in that subclass.

To override toString() for the Student class, let’s add the following method definition to the Student class:

public String toString() {
  return "My name is " + name +  " and I am a Student.";
}

Given this change, the revised Student class hierarchy is shown in the figure below. Note that both Object and Student contain implementations of toString(). Now when the expression stu.toString() is invoked, the following, more informative, output is generated:

My name is Stu and I am a Student.

In this case, when Java encounters the method call stu.toString(), it invokes the toString() method that it finds in the Student class.

The Student class hierarchy with toString method.

Static Binding and Dynamic Binding

The mechanism that Java uses in these examples is known as dynamic binding, in which the association between a method call and the correct method implementation is made at run time. In dynamic binding a method call is bound to the correct implementation of the method at run time by the Java Virtual Machine (JVM).

Dynamic binding is contrasted with static binding, the mechanism by which the Java compiler resolves the association between a method call and the correct method implementation when the program is compiled. In order for dynamic binding to work, the JVM needs to maintain some kind of representation of the Java class hierarchy, including classes defined by the programmer. When the JVM encounters a method call, it uses information about the class hierarchy to bind the method call to the correct implementation of that method.

In Java, all method calls use dynamic binding except methods that are declared final or private. Final methods cannot be overridden, so declaring a method as final means that the Java compiler can bind it to the correct implementation. Similarly, private methods are not inherited and therefore cannot be overridden in a subclass. In effect, private methods are final methods and the compiler can perform the binding at compile time.

Polymorphism

Java’s dynamic-binding mechanism, which is also called late binding or run-time binding, leads to what is know as polymorphism. Polymorphism is a feature of object-oriented languages whereby the same method call can lead to different behaviors depending on the type of object on which the method call is made. The term polymorphism means, literally, having many (poly) shapes (morphs). Here’s a simple example:

 Object obj;                        // Static type: Object
 obj = new Student("Stu");          // Actual type: Student
 System.out.println(obj.toString());// Prints "My name is Stu..."
 obj = new OneRowNim(11);           // Actual type: OneRowNim
 System.out.println(obj.toString());// Prints "nSticks = 11, player = 1"

The variable obj is declared to be of type Object. This is its static or declared type. A variable’s static type never changes. However, a variable also has an actual or dynamic type. This is the actual type of the object that has been assigned to the variable. As you know, an Object variable can be assigned objects from any Object subclass. In the second statement, obj is assigned a Student object. Thus, at this point in the program, the actual type of the variable obj is Student. When obj.toString() is invoked in the third line, Java begins its search for the toString() method at the Student class, because that is the variable’s actual type.

In the fourth line, we assign a different class OneRowNim object to obj, thereby changing its actual type to OneRowNim. Thus, when obj.toString() is invoked in the last line, the toString() method is bound to the implementation found in the OneRowNim class.

Thus, we see that the same expression, obj.toString(), is bound alternatively to two different toString() implementations, based on the actual type of the object, obj, on which it is invoked. This is polymorphism.

The important point here is that polymorphism occurs when an overridden method is called on a superclass variable, e.g., obj. In such a case, the actual method implementation that is invoked is determined at run time. The determination depends on the type of object that was assigned to the variable. Thus, we say that the method call obj.toString() is polymorphic because it is bound to different implementations of toString() depending on the actual type of the object that is bound to obj.

Polymorphism and Object-Oriented Design

Now that we understand how inheritance and polymorphism work in Java, it will be useful to consider an example that illustrates how these mechanisms can be useful in designing classes and methods.

The System.out.print() and System.out.println() methods are examples of overloaded methods, that is, methods that have the same name but different parameter lists. Remember that a method’s signature involves its name, plus the type, number, and order of its parameters. Methods that have the same name but different parameters are said to be overloaded.

Here are the signatures of some of the different print() and println() methods:

print(char c);           println(char c);
print(int i);            println(int i);
print(double d);         println(double d);
print(float f);          println(float f);
print(String s);         println(String s);
print(Object o);         println(Object o);

Basically, there is a print() and println() method for every type of primitive data, plus methods for printing any type of object. When Java encounters an expression involving print() or println() it chooses which particular method to call. To determine the correct method, Java relies on the differences in the signatures of the various print() methods. For example, because its argument is an int, the expression print(5) is associated with the method whose signature is print(int i) be cause its parameter is an int.

Using super to Refer to the Superclass

One question that might occur to you is: Once you override the default toString() method, is it then impossible to invoke the default method on a Student object? The default toString() method (and any method from an object’s superclass) can be invoked using the super keyword. For example, suppose that within the Student class, you wanted to concatenate the result of both the default and the new toString() methods. The following expression would accomplish that:

super.toString() + toString()

The super keyword specifies that the first toString() is the one implemented in the superclass. The second toString() refers simply to the version implemented within the class. We will see additional examples of using the super keyword in the following sections.

Inheritance and Constructors

Java’s inheritance mechanism applies to a class’s public and protected instance variables and methods. It does not apply to a class’s constructors. To illustrate some of the implications of this language feature, let’s define a subclass of Student called CollegeStudent:

public class CollegeStudent extends Student {
    public CollegeStudent() { }
    public CollegeStudent(String s) {
        super(s);
    }
    public String toString() {
       return "My name is " + name +  
               " and I am a CollegeStudent.";
    }
}

Because CollegeStudent is a subclass of Student, it inherits the public and protected instance methods and variables from Student. So, a CollegeStudent has an instance variable for name and it has a public getName() method. Recall that a protected element, such as the name variable in the Student class, is accessible only within the class and its subclasses. Unlike public elements, it is not accessible to other classes.

Note how we have implemented the CollegeStudent(String s) constructor. Because the superclass’s constructors are not inherited, we have to implement this constructor in the subclass if we want to be able to assign a CollegeStudent’s name during object construction. The method call, super(s), is used to invoke the superclass constructor and pass it s, the student’s name. The superclass constructor will then assign s to the name variable.

As we have noted, a subclass does not inherit constructors from its superclasses. However, if the subclass constructor does not explicitly invoke a superclass constructor, Java will automatically invoke the default superclass constructor—in this case, super(). By default superclass constructor we mean the constructor that has no parameters. For a subclass that is several layers down in the hierarchy, this automatic invoking of the super() constructor will be repeated upwards through the entire class hierarchy. Thus when a CollegeStudent is constructed, Java will automatically call Student() and Object(). Note that if one of the superclasses does not contain a default constructor, this will result in a syntax error.

If you think about this, it makes good sense. How else will the inherited elements of the object be created? For example, in order for a CollegeStudent to have a name variable, a Student object, where name is declared, must be created. The CollegeStudent constructor then extends the definition of the Student class. Similarly, in order for a Student object to have the attributes common to all objects, an Object instance must be created and then extended into a Student.

Thus, unless a constructor explicitly calls a superclass constructor, Java will automatically invoke the default superclass constructors. It does this before executing the code in its own constructor. For example, if you had two classes, A and B, where B is a subclass of A, then whenever you create an instance of B, Java will first invoke A’s constructor before executing the code in B’s constructor so the base class is instantiated. Thus, Java’s default behavior during construction of B is equivalent to the following implementation of B’s constructor:

public B() {
    A();   // Call the superconstructor. Java does this automatically if not specified
    // Now continue with this constructor's code
}

For example,

public class A {
     public A() { System.out.println("A"); }
 }
class B extends A {
     public B() { System.out.println("B"); }
 }
class C extends B {
     public C() { System.out.println("C"); }
 }

 // Determine the output.
 A a = new A();
 B b = new B();
 C c = new C();

It will print:

A
A
B
A
B
C

Abstract Classes, Interfaces, and Polymorphism

In Java, there are three kinds of polymorphism:

  • Overriding an inherited method.
  • Implementing an abstract method.
  • Implementing a Java interface.

In the previous section we saw examples of the first type of polymorphism. All forms of polymorphism are based on Java’s dynamic binding mechanism. In this section we will develop an example that illustrates the other two types of polymorphism and discuss some of the design implications involved in choosing one or the other approach.

Implementing an Abstract Method

An important feature of polymorphism is the ability to invoke a polymorphic method that has been defined only abstractly in the superclass. To illustrate this feature, we will develop a hierarchy of simulated animals that make characteristic animal sounds, an example that is widely used to illustrate polymorphism.

As we all know from our childhood, animals have distinctive ways of speaking. A cow goes “moo”; a pig goes “oink”; and so on. Let’s design a hierarchy of animals that simulates this characteristic by printing the characteristic sounds that these animals make. We want to design our classes so that any given animal will return something like “I am a cow and I go moo,” when we invoke the toString() method. Moreover, we want to design this collection of classes so that it is extensible—that is, so that we can continue to add new animals to our menagerie without having to change any of the code in the other classes.

The figure below provides a summary of the design we will implement. The Animal class is an abstract class. That’s why its name is italicized in the figure. The reason that this class is abstract is because its speak() method is an abstract method, which is a method definition that does not contain an implementation. That is, the method definition contains just the method’s signature, not its body. Any class that contains an abstract method, must itself be declared abstract.

The Animal class hierarchy.

Here is the definition of the Animal class:

public abstract class Animal {
    protected String kind; // Cow, pig, cat, etc.
         
    public Animal()  {  }
    public String toString() {
        return "I am a " + kind + " and I go " + speak();
    }
    public abstract String speak();   // Abstract method
}

Note how we declare the abstract method (speak()) and the abstract class. Because one or more of its methods is not implemented, an abstract class cannot be instantiated. That is, you cannot say:

Animal animal = new Animal(); // Error: Animal is abstract

Even though it is not necessary, we give the Animal class a constructor. If we had left this off, Java would have supplied a default constructor that would be invoked when Animal subclasses are created.

Java has the following rules on using abstract methods and classes.

  • Any class containing an abstract method must be declared an class.
  • An abstract class cannot be instantiated. It must be subclassed.
  • A subclass of an abstract class may be instantiated only if it implements all of the superclass’s abstract methods. A subclass that implements only some of the abstract methods must itself be declared abstract.
  • A class may be declared abstract even it contains no abstract methods. It could, for example, contain instance variables that are common to all its subclasses.

Even though an abstract method is not implemented in the superclass, it can be called in the superclass. Indeed, note how the toString() method calls the abstract speak() method. The reason that this works in Java is due to the dynamic binding mechanism. The polymorphic speak() method will be defined in the various Animal subclasses. When the Animal.toString() method is called, Java will decide which actual speak() method to call based on what subclass of Animal is involved.

public class Cat extends Animal {
    public Cat() {
        kind = "cat";
    }
    public String speak() {
        return "meow";
    }
}

public class Cow extends Animal {
    public Cow() {
        kind = "cow";
    }
    public String speak() {
        return "moo";
    }
}

In each case the subclass extends the Animal class and provides its own constructor and its own implementation of the speak() method. Note that in their respective constructors, we can refer to the kind instance variable, which is inherited from the Animal class. By declaring kind as a protected variable, it is inherited by all Animal subclasses but hidden from all other classes. On the other hand, if kind had been declared public, it would be inherited by Animal subclasses, but it would also be accessible to every other class, which would violate the information hiding principle.

Given these definitions, we can now demonstrate the power and flexibility of inheritance and polymorphism. Consider the following code segment:

Animal animal = new Cow();
System.out.println(animal.toString()); // A cow goes moo

animal = new Cat();
System.out.println(animal.toString()); // A cat goes meow

We first create a Cow object and then invoke its (inherited) toString() method. It returns, “I am a cow and I go moo.” We then create a Cat object and invoke its (inherited) toString() method, which returns, “I am a cat and I go meow.” In other words, Java is able to determine the appropriate implementation of speak() at run time in each case. The invocation of the abstract speak() method in the Animal.toString() method is a second form of polymorphism.

What is the advantage of polymorphism here? The main advantage is the extensibility that it affords our Animal hierarchy. We can define and use completely new Animal subclasses without redefining or recompiling the rest of the classes in the hierarchy. Note that the toString() method in the Animal class does not need to know what type of Animal subclass will be executing its speak() method. The toString() method will work correctly for any subclass of Animal because every non-abstract subclass of Animal must implement the speak() method.

To get a better appreciation of the flexibility and extensibility of this design, it might be helpful to consider an alternative design that does not use polymorphism. One such alternative would be to define each Animal subclass with its own speaking method. A Cow would have a moo() method; a Cat would have a meow() method; and so forth. Given this design, we could use a switch statement to select the appropriate method call. For example, consider the following method definition:

public String talk(Animal a) {
  if (a instanceof Cow)
     return "I am a " + kind + " and I go " + a.moo();
  else if (a instanceof Cat)
     return "I am a " + kind + " and I go " + a.meow();
  else
    return "I don't know what I am";
}

In this example, we introduce the instanceof operator, which is a built-in boolean operator. It returns true if the object on its left-hand side is an instance of the class on its right-hand side.

The talk() method would produce more or less the same result. If you call talk(new Cow()), it will return “I am a cow and I go moo.” However, with this design, it is not possible to extend the Animal hierarchy without rewriting and recompiling the talk() method.

Thus, one of the chief advantages of using polymorphism is the great flexibility and extensibility it affords. We can define new Animal subclasses and define their speak() methods. These will all work with the toString() method in the Animal class, without any need to revise that method.

Another advantage of using abstract methods is the control that it gives the designer of the Animal hierarchy. By making it an abstract class with an abstract speak() method, any non-abstract Animal subclass must implement the speak() method. This lends a great degree of predictability to the subclasses in the hierarchy, making it easier to use them in applications.

Following the examples in this section, define an Animal subclass named Pig, which goes “oink.”

Show how you would have to modify the talk() method defined above to incorporate the Pig class.

Implementing a Java Interface

A third form of polymorphism results through the implementation of Java interfaces, which are like classes but contain only abstract method definitions and constants (final) variables. An interface cannot contain instance variables.

The designer of an interface specifies what methods will be implemented by classes that implement the interface. This is similar to what we did when we implemented the abstract speak() method in the animal example. The difference between implementing a method from an interface and from an abstract superclass is that a subclass extends an abstract superclass but it implements an interface.

Java’s interface mechanism gives us another way to design polymorphic methods. To see how this works, we will provide an alternative design for our animal hierarchy. Rather than defining speak() as an abstract method within the Animal superclass, we will define it as an abstract method in the Speakable interface:

public interface Speakable {
    public String speak();
}

public class Animal {
    protected String kind; // Cow, pig, cat, etc.
    public Animal()  {  }
    public String toString() {
        return "I am a " + kind + " and I go " + 
               ((Speakable)this).speak();
    }
}

Note the differences between this definition of Animal and the previous definition. This version no longer contains the abstract speak() method. Therefore, the class itself is not an abstract class. However, because the speak() method is not declared in this class, we cannot call the speak() method in the toString() method, unless we cast this object into a Speakable object.

Given these definitions, Animal subclasses will now extend the Animal class and implement the Speakable interface:

public class Cat extends Animal implements Speakable {
    public Cat() { kind = "cat"; }
    public String speak() { return "meow";  }
}
public class Cow extends Animal implements Speakable {
    public Cow() { kind = "cow";  }
    public String speak() { return "moo";  }
}

To implement a Java interface, one must provide a method implementation for each of the abstract methods in the interface. In this case there is only one abstract method, the speak() method.

Note, again, the expression from the Animal.toString() class

((Speakable)this).speak();

which casts this object into a Speakable object. The reason that this cast is required is because an Animal does not necessarily have a speak() method. A speak() method is not defined in the Animal class. However, the Cat subclass of Animal does implement a sleep() method as part of its Speakable interface. Therefore, in order to invoke speak() on an object from one of the Animal subclasses, the object must actually be a Speakable and we must perform the cast as shown here.

This illustrates, by the way, that a Cat, by virtue of extending the Animal class and implementing the Speakable interface, is both an Animal and a Speakable. In general, a class that implements an interface, has that interface as one of its types. Interface implementation is itself a form of inheritance. A Java class can be a direct subclass of only one superclass. But it can implement any number of interfaces.

Given these definitions of the Cow and Cat subclasses, the following code segment will produce the same results as in the previous section.

Animal animal = new Cow();
System.out.println(animal.toString()); // A cow goes moo
animal = new Cat();
System.out.println(animal.toString()); // A cat goes meow

Although the design is different, both approaches produce the same result.

License

#java #inheritance

You May Also Enjoy


If you like this post, please share using the buttons above. It will help CodeAhoy grow and add new content. Thank you!


Speak Your Mind