Java 101: Object-oriented language basics, Part 7: Polymorphism and abstract classes

Learn about the many varieties of polymorphism in object-oriented programming and find out how abstract classes can be used to accommodate generalities in your Java class hierarchies

For several months now the Java 101 column has been touring Java's object-oriented language basics. Topics covered include class declaration, object creation, field/method declaration and access, object composition, object layering, multiple implementation inheritance, the root of all classes, and interfaces (with multiple interface inheritance). It seems fitting to conclude the tour with two topics that are especially confusing to novice programmers: polymorphism and abstract classes and methods. The difference between abstract classes and interfaces is also explained.

Polymorphism, a fundamental object-oriented programming principle, tends to confuse novice Java developers. Part of the bewilderment stems from the fact that, although there are four types of polymorphism, Java officially supports only three of them (at the moment). This article attempts to clear away the clouds surrounding polymorphism by exploring each polymorphism type -- with emphasis on the inclusion polymorphism category.

After discussing polymorphism, I turn to abstract classes and abstract methods. Abstract classes prevent developers from creating objects out of those classes that represent generic concepts. In addition, abstract classes often work with inclusion polymorphism. Though abstract classes and interfaces do resemble one another, they also differ, and I will explain how in this article.

Polymorphism

The quality or state of assuming different forms, or shapes, is known as polymorphism. Water provides a good real-world example of polymorphism. When frozen, water turns into ice. When that ice melts, a liquid appears. Boil that liquid and you end up with a gas. Computer languages also manifest polymorphism in different ways. As Wm. Paul Rogers points out in his excellent article "Reveal the Magic Behind Subtype Polymorphism," there are four kinds of polymorphism at the computer-language level: coercion, overloading, parametric, and inclusion.

Coercion polymorphism refers to a single operation serving several types through implicit type conversion. For example, the subtraction operation manifests itself in source code through the subtraction operator (-). That operator allows you to subtract an integer from another integer and a floating-point value from another floating-point value. However, if one operand is an integer and the other operand a floating-point value, the compiler must convert the integer's operand type to floating-point. Otherwise, a type error occurs -- because Java's subtraction operation does not subtract integers from floating-point values, or vice versa. Another example of coercion polymorphism involves method calls. If a subclass declares a method with a superclass parameter, and if a call is made to that method with a subclass object reference, the compiler implicitly converts the subclass reference type to the superclass reference type. That way, only superclass-defined operations are legal (without explicit type casts) in the method.

Overloading polymorphism refers to using a single identifier for different operations. For example, the + identifier -- I use the term identifier in a general sense -- signifies any one of several operations based on its operands' types. If both operands have integer types, the integer addition operation occurs. Similarly, if both operands have floating-point types, a floating-point operation occurs. Finally, if those operands are strings, string concatenation is guaranteed to be the operation.

Along with its language-defined operator overloading, Java also permits method names to overload -- as long as the number and/or types of each method's parameters differ. That way, the same method-name identifier can apply to different operations.

Many developers do not regard the coercion and overloading polymorphism categories as true forms of polymorphism. At close inspection, coercion and overloading are seen as convenient type conversion aids and syntactic sugar. In contrast, parametric and inclusion polymorphism are considered genuine polymorphism.

Parametric polymorphism refers to a class declaration that allows the same field names and method signatures to associate with a different type in each instance of that class. For example, you might create a LinkedList class with a data field that holds any type of data. To allow for proper type checking at compile time, you do not want to give that field Object type (as in Object data;) as the compiler cannot inform you if the code attempts to perform invalid operations on data, because only the JVM knows the actual type of data at runtime. However, you do not want to tie data to a specific type in your source code, because you lose the benefit of being able to store different object types in your LinkedList objects. To achieve the best of both worlds, parametric polymorphism gives you the benefits of static type checking -- which alerts you to attempts that perform invalid operations on data -- and allows data to hold different object types. Probably the best-known example of parametric polymorphism is the C++ template language feature. Although Java does not officially support parametric polymorphism, developers expect Sun to add that support to version 1.5 of the SDK, which is scheduled for a 2003 release. For more information on parametric polymorphism, I refer you to Eric Allen's well-written article "Behold the Power of Parametric Polymorphism."

Inclusion polymorphism refers to a situation in which a type can be another type's subtype. Every subtype value can appear in a supertype context, where the execution of the supertype's operations (on that value) results in the execution of the subtype's equivalent operations. For that reason, inclusion polymorphism is also known as subtype polymorphism. To understand inclusion (or subtype) polymorphism, you must understand how Java binds method calls to classes and objects.

Method binding

Recall from an earlier article in this series that Java supports two categories of methods: class (also known as static) and instance (also known as nonstatic). Class methods associate with classes, and instance methods associate with objects. When you call a method, Java binds that method call to either a class or an object, depending on that method's category.

Listing 1 shows how Java statically binds class method calls to classes at compile time:

Listing 1. SMB.java

// SMB.java
class Superclass
{
   static void method ()
   {
      System.out.println ("Superclass method");
   }
}
class Subclass extends Superclass
{
   static void method ()
   {
      System.out.println ("Subclass method");
   }
}
class SMB
{
   public static void main (String [] args)
   {
      Superclass sc = new Superclass ();
      sc.method ();
      sc = new Subclass ();
      sc.method ();
   }
}

SMB declares a class hierarchy consisting of Superclass and Subclass classes. Each of those classes declares a class method named method(). A third class, SMB, declares a main() method that does the following:

  1. Creates a Superclass object
  2. Assigns that object's reference to sc
  3. Calls method() by (apparently) using sc's reference
  4. Creates a Subclass object
  5. Assigns that object's reference to sc
  6. Calls method() by (apparently) using sc's reference

When run, SMB produces the following output:

Superclass method
Superclass method

The output might surprise you, but remember that class methods associate with classes, not objects. Even though SMB's sc.method() calls give the impression that main() calls instance methods, main() calls only a single method() statically bound to Superclass. SMB's main() method could just as easily achieve the same output by replacing main()'s code with two consecutive Superclass.method (); calls. As a result, inclusion/subtype polymorphism does not exist with statically bound method calls. (Note: Calling class methods via object reference variables is not wise because it clouds the fact that those methods are class methods.)

Contrary to how Java treats class methods, Java dynamically binds instance method calls to objects at runtime, making inclusion/subtype polymorphism possible. Listing 2 demonstrates this:

Listing 2. DMB1.java

// DMB1.java
class Superclass
{
   void method ()
   {
      System.out.println ("Superclass method");
   }
}
class Subclass extends Superclass
{
   void method ()
   {
      System.out.println ("Subclass method");
   }
}
class DMB1
{
   public static void main (String [] args)
   {
      Superclass sc = new Superclass ();
      sc.method ();
      sc = new Subclass ();
      sc.method ();
   }
}

DMB1's source code is nearly identical to SMB's source code. Apart from its class name, DMB1 only differs in the absence of the static keyword from the method() signatures in Superclass and Subclass. When run, DMB1 outputs the following:

Superclass method
Subclass method

DMB1's output differs from SMB1's because the JVM dynamically locates the correct method() to call based on the object reference appearing in sc. In the first method() call, the JVM uses the Superclass object reference it retrieved from sc to locate Superclass's method(). In the second method() call, the JVM uses the Subclass object reference it retrieved from sc to locate Subclass's method(). And that is inclusion/subtype polymorphism at work.

Because Java dynamically binds instance method calls to objects at runtime, you can use inclusion/subtype polymorphism with either a single class hierarchy or with interfaces that extract commonality from multiple class hierarchies, as Listing 3 demonstrates:

Listing 3. DMB2.java

// DMB2.java
interface StartStop
{
   void start ();
   void stop ();
}
class Conference implements StartStop
{
   public void start ()
   {
      System.out.println ("Start the conference.");
   }
   public void stop ()
   {
      System.out.println ("Stop the conference.");
   }
}
class PoliticalConvention extends Conference
{
   public void start ()
   {
      System.out.println ("Introduce the guest speaker.");
   }
   public void stop ()
   {
      System.out.println ("Grab some refreshments.");
   }
}
class Vehicle implements StartStop
{
   public void start ()
   {
   }
   public void stop ()
   {
   }
}
class Car extends Vehicle
{
   public void start ()
   {
      System.out.println ("Insert key in ignition and turn.");
   }
   public void stop ()
   {
      System.out.println ("Turn key in ignition and remove.");
   }
}
class LawnMower extends Vehicle
{
   public void start ()
   {
      System.out.println ("Move sliding switch to on and pull cord.");
   }
   public void stop ()
   {
      System.out.println ("Move sliding switch to off.");
   }
}
class DMB2
{
   public static void main (String [] args)
   {
      Vehicle v = new LawnMower ();
      StartStop [] ss =
      {
         new Car (),
         v,
         new PoliticalConvention (),
         new Conference ()
      };
      for (int i = 0; i < ss.length; i++)
      {
           ss [i].start ();
           ss [i].stop ();
           System.out.println ("");
      }
   }
}

DMB2 introduces a pair of class hierarchies. The Conference and Vehicle root classes in each hierarchy implement the StartStop interface, and the subclasses override the respective implementations of StartStop's methods.

The main() method in the code above creates a LawnMower object and assigns its reference to Vehicle's object reference variable v. Then, main() creates an array of references to various objects, whose classes explicitly or implicitly (through a superclass) implement StartStop -- with one of the array's elements including v's contents. A for statement loops over those array elements, and each iteration calls an object's start() and stop() methods. To make the right call, the JVM locates the correct method by examining and following each array element's object reference to the object's memory area. There, the JVM can obtain the method's bytecode instructions.

When run, DMB2 produces the following output:

Insert key in ignition and turn.
Turn key in ignition and remove.
Move sliding switch to on and pull cord.
Move sliding switch to off.
Introduce the guest speaker.
Grab some refreshments.
Start the conference.
Stop the conference.

Inclusion/subtype polymorphism is a natural part of Java. The underlying dynamic method-binding mechanism that initiates this form of polymorphism kicks into action whenever you call an instance method, regardless of whether an object reference variable has class or interface type. Nevertheless, to achieve inclusion/subtype polymorphism's full benefits, you must work with class hierarchies, where subclasses introduce methods that override superclass methods.

Abstract classes

Like inclusion/subtype polymorphism, abstract classes are useful in the context of class hierarchies. When you design a class hierarchy, classes near the top of that hierarchy typically represent generic concepts. As such, you do not create objects from those classes; instead, you normally create objects from more specialized subclasses towards the hierarchy's bottom. Although you probably know which classes in the hierarchy to use for object creation and which ones to avoid, how do you prevent other developers from creating objects from the top classes in the hierarchy (assuming you distribute that hierarchy's classes -- as classfiles -- to other developers)? Permitting such behavior would allow developers to create meaningless objects, as Listing 4's Cats1 application demonstrates:

Listing 4. Cats1.java

// Cats1.java
class Cat
{
   String getInfo ()
   {
      return "?";
   }
}
class Lion extends Cat
{
   String getInfo ()
   {
      return "king of the jungle";
   }
}
class Cheetah extends Cat
{
   String getInfo ()
   {
      return "great speed";
   }
}
class Tiger extends Cat
{
   String getInfo ()
   {
      return "has stripes";
   }
}
class Cats1
{
   public static void main (String [] args)
   {
      Cat [] cats =
      {
         new Lion (),
         new Cheetah (),
         new Tiger (),
         new Cat ()
      };
      for (int i = 0; i < cats.length; i++)
           System.out.println (cats [i].getInfo ());
   }
}

Cats1 presents three hierarchies of cat-related classes. At the top of those hierarchies sits a Cat class. Even though that class is basically empty, its purpose is to extract commonality found among all cats. Cats1 confines that commonality to a single getInfo() method, and subclasses override getInfo() to return cat-specific information.

Cats1's main() method declares an array of Cat objects and assigns a new Cat object to each entry. It makes sense to create Lion, Cheetah, and Tiger objects, because those objects represent specific kinds of cats. But does it also make sense to create a Cat object and assign its reference to an array entry? Does Cat represent a specific kind of cat? I think not. My opinion stems from the following Cats1 output:

king of the jungle
great speed
has stripes
?

That final ? signifies the output from calling Cat's getInfo() method. The question mark indicates that the code cannot provide specific information about something so generic. Ideally, you do not want such a situation to crop up in your class hierarchies.

Fortunately, Java provides a way to prevent object creation from generic classes. Prefixing a class signature with the abstract keyword results in an abstract class. Any attempt to create an object from an abstract class causes a compiler error. Listing 5's Cats2 source code demonstrates an abstract class declaration:

Listing 5. Cats2.java

// Cats2.java
abstract class Cat
{
   abstract String getInfo ();
}
class Lion extends Cat
{
   String getInfo ()
   {
      return "king of the jungle";
   }
}
class Cheetah extends Cat
{
   String getInfo ()
   {
      return "great speed";
   }
}
class Tiger extends Cat
{
   String getInfo ()
   {
      return "has stripes";
   }
}
class Cats2
{
   public static void main (String [] args)
   {
      Cat [] cats =
      {
         new Lion (),
         new Cheetah (),
         new Tiger (),
//         new Cat ()
      };
      for (int i = 0; i < cats.length; i++)
           System.out.println (cats [i].getInfo ());
   }
}

Cats2 prefixes the Cat class signature with keyword abstract. To prove that you cannot create an object from Cat, uncomment the new Cat () line in the above cats array declaration and attempt to compile. You will receive a compiler error message.

Cats2 also demonstrates an abstract method: Cat's getInfo(). Prefixing a method signature with keyword abstract accomplishes the following tasks:

  1. Signifies that the method has no associated code body
  2. Indicates that the class declaring that method must have a class signature prefixed by keyword abstract
  3. Indicates that any subclass inheriting but not overriding that method is automatically regarded as abstract

You introduce an abstract method into an abstract class when that method identifies a common behavior that manifests itself (in subclasses) in different ways. For example, the get-information behavior found in Cats2 manifests itself somewhat differently in the Lion, Cheetah, and Tiger subclasses. Although each method currently returns a different string literal, you might change each method to contain additional logic unique to its class. Also, an abstract method's presence in an abstract superclass allows you to assign a subclass object reference to the superclass object reference variable and (through inclusion/subtype polymorphism) call the more specific subclass method.

After studying Cats2's Cat class, you might have the impression that abstract classes can declare only abstract methods. However, abstract classes can also declare constructors, other nonabstract methods, and fields, as shown in Listing 6:

Listing 6. Cats3.java

// Cats3.java
abstract class Cat
{
   private String name;
   Cat (String name)
   {
      this.name = name;
   }
   String getName ()
   {
      return name;
   }
   abstract String getInfo ();
}
class Lion extends Cat
{
   Lion (String name)
   {
      super (name);
   }
   String getInfo ()
   {
      return "king of the jungle";
   }
}
class Cheetah extends Cat
{
   Cheetah (String name)
   {
      super (name);
   }
   String getInfo ()
   {
      return "great speed";
   }
}
class Tiger extends Cat
{
   Tiger (String name)
   {
      super (name);
   }
   String getInfo ()
   {
      return "has stripes";
   }
}
class Cats3
{
   public static void main (String [] args)
   {
      Cat [] cats =
      {
         new Lion ("Donald"),
         new Cheetah ("Lucifer"),
         new Tiger ("Xena"),
//         new Cat ()
      };
      for (int i = 0; i < cats.length; i++)
           System.out.println (cats [i].getName () + " " +
                               cats [i].getInfo ());
   }
}

Cats3 declares a Cat class with an instance field (name), a constructor that initializes that field, and a getter method (getName()) that returns that field's value. (The name field and its getter method appear in Cat because all cats have names -- and we want to factor out commonality to prevent redundancy.) Despite the presence of the Cat(String name) constructor, you cannot create an object from the abstract Cat class. So why have that constructor? Notice that each subclass declares a constructor that passes its name parameter value to the Cat superclass constructor through the super (name); superclass constructor call. Cat's constructor initializes Cat's private name field because subclasses can't access a superclass's private fields. When we create a Lion, Cheetah, or Tiger object, we want to ensure that each object's internal Cat layer properly initializes. That often happens by having the subclass constructor pass information from its constructor to the superclass constructor.

Abstract classes versus interfaces

Developers often inquire about the difference between abstract classes and interfaces. After all, you can declare an abstract class containing only abstract methods and/or constants, which is exactly what an interface contains. Do these two language features differ?

Abstract classes belong to class hierarchies. They represent generic entity categories and typically sit at the top of such hierarchies. Furthermore, developers often factor out commonality from more specific subclasses and place that commonality in less specific (and abstract) superclasses. For example, in Cat3, the Cat class represents the generic cat category: Cat presents that state and those behaviors common to all cats. (Obviously, Cat is not complete.)

In contrast, interfaces typically represent capabilities common to diverse class hierarchies; they do not represent entity categories. For example, in DMB2, the StartStop interface factors out the capabilities of starting and stopping entities. (DMB2 allows conference and vehicle entities to start and stop.)

When you properly combine abstract classes and interfaces, you achieve powerful program architectures, as shown in Listing 7:

Listing 7. DMB3.java

// DMB3.java
interface StartStop
{
   void start ();
   void stop ();
}
abstract class Conference implements StartStop
{
}
class PoliticalConvention extends Conference
{
   public void start ()
   {
      System.out.println ("Introduce the guest speaker.");
   }
   public void stop ()
   {
      System.out.println ("Grab some refreshments.");
   }
}
abstract class Vehicle implements StartStop
{
   private String manufacturer;
   Vehicle (String manufacturer)
   {
      this.manufacturer = manufacturer;
   }
   String getManufacturer ()
   {
      return manufacturer;
   }
}
class Car extends Vehicle
{
   Car (String manufacturer)
   {
      super (manufacturer);
   }
   public void start ()
   {
      System.out.println ("Insert key in ignition and turn.");
   }
   public void stop ()
   {
      System.out.println ("Turn key in ignition and remove.");
   }
}
class LawnMower extends Vehicle
{
   LawnMower (String manufacturer)
   {
      super (manufacturer);
   }
   public void start ()
   {
      System.out.println ("Move sliding switch to on and pull cord.");
   }
   public void stop ()
   {
      System.out.println ("Move sliding switch to off.");
   }
}
class DMB3
{
   public static void main (String [] args)
   {
      Vehicle v = new LawnMower ("Black and Decker");
      StartStop [] ss =
      {
         new Car ("General Motors"),
         v,
         new PoliticalConvention (),
      };
      for (int i = 0; i < ss.length; i++)
      {
           ss [i].start ();
           ss [i].stop ();
           if (ss [i] instanceof Vehicle)
               System.out.println ("Vehicle manufacturer = " +
                                   ((Vehicle) ss [i]).getManufacturer ());
           System.out.println ("");
      }
   }
}

DMB3 resembles DMB2 in that DMB3 also introduces two class hierarchies. One hierarchy deals with conferences, and the other deals with vehicles. However, unlike DMB2, DMB3 declares Conference and Vehicle as abstract. (Conference is abstract because it represents a generic conference concept, instead of something more specific, like a political conference. Vehicle is abstract for similar reasons.) Because those classes are abstract, they do not provide code bodies for start() and stop(). (Note: Though Conference and Vehicle do not provide code bodies for StartStop's methods, those classes still implement the StartStop interface because they inherit that interface's abstract methods.)

In addition to reviewing DMB3's abstract classes and methods, look carefully at DMB3's main() method. That method demonstrates a use for the instanceof operator. In addition to starting and stopping each vehicle and conference object, DMB3 determines whether the object is a type of vehicle by using instanceof. If an object is a vehicle, that object casts to Vehicle. Then, DMB3 calls the object's getManufacturer() method to retrieve the name of the vehicle's manufacturer, which it subsequently prints.

As you have just learned, abstract classes and interfaces can combine to form powerful program architectures. Abstract classes sit at the top of their respective class hierarchy pillars and factor out commonalities from within their respective hierarchies. Typically, abstract classes implement interfaces that extract common capabilities from each hierarchy. In some programs, those abstract classes provide code bodies for interface methods. In other programs, like DMB3, they do not.

Note: Recently, JavaWorld published an article by Wm. Paul Rogers that provides great clarity on when to use interfaces, when to use abstract classes, and when to combine those language features. I strongly recommend that you read "Maximize Flexibility with Interfaces and Abstract Classes."

Review

This article identified four kinds of polymorphism -- coercion, overloading, parametric, and inclusion -- and pointed out that Java does not officially support parametric polymorphism. I specifically focused on inclusion polymorphism, also called subtype polymorphism, and showed (through examples) that Java supports inclusion polymorphism by dynamically binding instance method calls to objects at runtime.

I then turned to abstract classes and described their usefulness in accommodating class hierarchy generalities. Finally, I compared abstract classes to interfaces and showed that both language features can combine to create powerful program architectures.

In next month's article, you'll learn how to initialize objects and classes.

Note: To help you further master the Java language, I have enhanced Java 101 with a study guide that will accompany each column. Each study guide provides additional material relevant to its companion Java 101 article. In it you will find a glossary, homework, homework solutions, my answers to your questions, tips and cautions, and other material.

This article is the first to have its own study guide; future and, eventually, prior Java 101 articles will also feature their own guides.

Jeff Friesen has been involved with computers for the past 20 years. He holds a degree in computer science and has worked with many computer languages. Jeff has also taught introductory Java programming at the college level. In addition to writing for JavaWorld, he wrote his own Java book for beginners -- Java 2 By Example (QUE, 2000) -- and helped write a second Java book, Special Edition Using Java 2 Platform (QUE, 2001). Jeff goes by the nickname Java Jeff (or JavaJeff). To see what he's working on, check out his Website at http://www.javajeff.com.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies