2-12 Polymorphism

Polymorphism is a feature related to inheritance and method overriding. The basic idea of polymorphism is that if you have an object of a child class, you can safely treat it as if it has the same type as the superclass.  I’ll demonstrate the features of polymorphism using our Person and Student classes from 2-10, here’s the UML diagram for reference:

2-12-umlclassdiagram1

Here’s an example:

And here is the output:

Person: James Bond (age: 37)
Person: Jimmy Lee (age: 16), is a Student with id #123456789
James Bond

We begin by importing the Person and Student classes from our people package. On lines 6 and 7 I declare two Person reference variables. Because of polymorphism, these variables may refer to any Person object or any object of a subclass of Person (i.e., Student). This is demonstrated on the next two lines. On line 9, we instantiate a Person object and assign p1 to refer to it (as usual). On line 10 notice that we are able to assign p2 to refer to a Student object and it works!  This is the first feature of polymorphism: a reference variable of a parent class can be used to refer to an object of a child class.

The reverse is not true:

This line would fail to compile because Person is not a child class of Student.

The second important aspect of polymorphism is shown on lines 12 and 13. Here, p1 and p2 are both reference variables of type Person, yet when we call the toString() method the JVM knows which version to call (i.e., the one from Person or the overridden one from Student). If a child class overrides a method in a parent class, the JVM determines which version of the method to call based on the object’s type (not the reference variable’s type).

In the case of p1, it refers to a Person object so the toString() method from the Person class is executed. In the case of p2, if refers to a Student object so the overridden toString() method from the Student class is called instead.  This is why method overriding plays an important role in polymorphism.

But there’s a catch! Even if a reference points to an object of a subclass, you can only use methods that are defined in the parent class. For example, despite the fact that p2 refers to a Student object, if we attempt the following:

The Java compiler will give a syntax error because getStudentId() is not a method of the Person class (and p2 was defined as a reference to a Person object).

This makes sense because since we declared p2 as a reference to a Person object, the Java compiler only knows what methods are available in the Person class.  Remember that objects are only created when the program is running.  The compiler will not know that p2 is going to refer to a Student object.  Although we are allowed to use a Person reference variable to point to an object of a child class, the Java compiler can’t be sure about what unique methods the child class object might have.  All the Java compiler can be sure of is that any child class of Person will have inherited all of the methods from Person.

Abstract Classes / Methods

Abstract methods and classes are also related to inheritance.  The best way to explain is with an example.

Let’s say we are designing a BankAccount class.  We know that every bank account has an account number and a customer name, so we add these as private instance variables to our class.  We also know that there will need to be child classes of our BankAccount class to represent specific types of accounts such as SavingsAccount and ChequingAccount.  As you would expect, savings and chequing accounts both have account numbers and customer names associated with them (which they will inherit from BankAccount), but there will also be unique instance variables and methods for each type of child account.

We also know that every child class of BankAccount will need a unique method to compute interest, however our BankAccount class is too general to completely define such a method yet.  It’s not until we define the SavingsAccount and ChequingAccount classes that it would make sense to actually define a complete computeInterest() method.  This is where the abstract keyword comes in!

On line 17 we use the abstract keyword when defining a computeInterest() method.  Notice that this definition only consists of a method signature and the method has no body, not even empty curly braces { }.  This kind of method is called an abstract method.  An abstract method may only be declared as public or protected (not private).

Furthermore, because we have not defined a body for the computeInterest() method it is not possible to directly instantiate a BankAccount object.   The Java compiler requires us to declare the class itself as abstract (line 1).  An abstract class is a class that contains one (or more) abstract methods and therefore cannot be instantiated.

It becomes the responsibility of any direct (or indirect) child class of BankAccount to override the computeInterest() method and provide a complete method body for it.  Let’s define a SavingsAccount class:

On lines 12 to 16 you can see that we are overriding the inherited abstract computeInterest() method.  We do not use the abstract keyword here because we are now defining a complete method body for it.  Once this is done we can instantiate SavingsAccount objects.

Note that if the SavingsAccount class above did not override computeInterest(), then we would need to declare SavingsAccount as an abstract class too.  It would then become the responsibility of a direct (or indirect) child class of SavingsAccount to override computeInterest().  At some point in the class hierarchy the computeInterest() method must have a complete method body defined.

Abstract Classes / Methods in UML

To indicate an abstract class or method in UML you simply use an italic font:

Here, the BankAccount class name, and the computeInterest() method in the BankAccount class are both italicized.

You Try!

  1. Consider the following Java classes:

    Try drawing a UML diagram of the classes above, then analyze what the output of the main() method below would be (on paper).  When you’re done, test it in Dr. Java to see if you are correct.
  2. Your Rectangle and Oval classes both have their own calcArea() methods with identical signatures but that perform different calculations depending on the shape.  You did not “factor-out” the calcArea() method into the FillableShape parent class for two reasons.  First, calcArea() performs different calculations depending on the type of shape object so it would really make no sense to inherit such a method from FillableShape.  Second,  the concept of a FillableShape is too general to be able to define a specific area calculation for it anyway.  However, as the designer of the FillableShape class, you might want to enforce that any child classes implement a calcArea() method.  Add an abstract calcArea() method to your FillableShape class — don’t forget to define the FillableShape class as abstract too.  No changes should be necessary to your Rectangle or Oval classes.
  3. Update your UML class hierarchy diagram to reflect the changes above.
  4. Try to instantiate a FillableShape object.  What happens?  Why does this make sense?