Skip to main content

Section 8.3 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.

Subsection 8.3.1 Implementing an Abstract Method

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.

Figure 8.3.1 provides a summary of the design. The Animal class is an abstract class. That's why its name is italicized in the UML diagram. 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.

Figure 8.3.1. The Animals 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 abstract 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.

Definitions for two such subclasses are shown in Listing 8.3.2.

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";
    }
}
Listing 8.3.2. Two Animal subclasses.

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 subclassesb ut it would also be accessible to every other class, a violation of the information hiding principle.

Given these definitions, we can now demonstrate the power and flexibility of inheritance and polymorphism. Run the code below to see it in action.

Activity 8.3.1.

Run the following code. Can you change what the cow and cat say?

Consider the following code segment from the main() method:

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.”

As this example shows, 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. Java's dynamic binding mechanism enables the Animal.toString() method to determine the type of Animal at run time subclass so that it will call the appropriate speak() method for that type of Animal.

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 as our polymorphic approach. 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. Imagine how unwieldy this would become if we want to add many different animals.

Exercises Self-Study Exercises

1. Pig subclass.

To see how easy it is to extend our hierarchy, define an Animal subclass named Pig, which goes “oink.”

2. Pig talk.

Modify the talk() method to incorporate the Pig class.

Subsection 8.3.2 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 (i.e., final variables).

An interface cannot contain instance variables. We have already seen interfaces, such as when we encountered the ActionListener interface in Chapter 4.

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.

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 (Fig. Figure 8.3.3).

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();
    }
}
Figure 8.3.3. Defining and using the Speakable interface.

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.

We encountered the cast operation in Chapter 5, where we used it with primitive types such as (int) and (char). Here, we use it to specify the actual type of some object. In this toString() example, this object is some type of Animal subclass, such as a Cat. The cast operation, (Speakable), changes the object's actual type to Speakable, which syntactically allows its speak() method to be called.

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 — the speak() method.

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

((Speakable)this).speak();

which casts this object into a Speakable object. The cast is required because the Animal class does not have a sleep() method. Therefore, in order to invoke speak() on an object from one of the Animal subclasses, the object must actually be a Speakable.

As defined here, 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

Exercises Exercises

1. Speakable interface.

Modify the code below to add a Pig to the hierarchy using the interface implementation.

Although the design is different, both approaches produce the same result. We will put off, for now, the question of how one decides whether to use an abstract method or a Java interface. We will get to this question when we design the TwoPlayerGame class hierarchy later in this chapter.

You have attempted of activities on this page.