There is more to object-oriented programming than just objects. Although encapsulating data and functionality within a class boundary goes a long way to improve the organization of complex programs, the crowning feature of object orientation is polymorphism, which is the ability to process objects related by inheritance through a single abstract interface, while the behavior of those objects varies dynamically according to their actual type. In this article I'll discuss how to use inheritance and polymorphism in Java.
Code Sharing
The first benefit of inheritance that meets the eye is code economy. Consider a simple Employee class like that in Figure 1. The crucial operation here is
computePay()(just ask any employee :-), which in this case gives time-and-a-half for overtime. What should you do to handle salaried employees? If you were to define aSalariedEmployeeclass, it would look just like classEmployeeexcept for the name of the class (and its constructor, of course) and the implementation ofcomputePay(). The fields and other methods and fields ofEmployeewould be repeated inSalariedEmployee, which is not only tedious but a maintenance headache, since both copies must be kept in sync. Inheritance allows you to defineSalariedEmployeeas an extension ofEmployee, as follows:class SalariedEmployee extends Employee { public SalariedEmployee(String name, double rate) { super(name, rate); } public double computePay() { return timeWorked * rate; } }All you have to do is say thatSalariedEmployee(the "subclass") extendsEmployee(the "superclass"), and then specify what is different aboutSalariedEmployee. ASalariedEmployeeobject inherits everything else fromEmployee, so it has thename,rate, andtimeWorkedfields, and it can also call the other threeEmployeemethods (getName(),getRate()andrecordTime()). TheSalariedEmployeeconstructor just passes its arguments to theEmployeeconstructor to initialize its inherited fields (that's what the super method does).SalariedEmployee.computePay()overridesEmployee.computePay()by interpretingtimeWorkedas the number of pay periods. Whenever you invokecomputePayon aSalariedEmployeeobject,SalariedEmployee.computePay()executes, notEmployee.computePay().Dynamic Binding
Another nice thing about inheritance is that it allows you to express class relationships in your code. In real life, a salaried employee is an employee, that is, anything you can say about employees also applies to salaried employees. The
extendskeyword in Java works analogously by implementing an is-a relationship between a subclass and its superclass: anywhere you can use anEmployeeobject you can use aSalariedEmployeeobject. The program in Figure 2 illustrates this in two ways. First, notice that it stores a handle to aSalariedEmployeeobject in an array ofEmployee. Since aSalariedEmployeeis anEmployee, this makes sense. Storing a handle to a subclass in a superclass handle is so common and crucial to object-oriented programming that it has a name: upcasting, so called because we usually draw class diagrams with superclasses positioned below subclasses (see Figure 3). The type of object a variable actually refers to at runtime is called its dynamic type.The second use of the is-a relationship is in the
pay()method.pay()doesn't care whether the variableerefers to an instance of the superclass or the subclass. It treats its parameter as anEmployee, and thus only calls methods named in theEmployeeclass. Since aSalariedEmployeeis anEmployee, thecomputePay()method applies. BUT -- it wantsSalariedEmployee.computePay()to execute, notEmployee.computePay(). How can this happen wheneis declared as a handle to anEmployeeobject?The answer is dynamic binding, which maps a method name to an implementation according to the object's dynamic type. The expression
e.computePay()will call the correct version ofcomputePay()according to whethereis referring to anEmployeeor aSalariedEmployeeat the moment. The use of dynamic binding of methods in an inheritance hierarchy is called polymorphism.Unless you specify otherwise, all methods in Java bind dynamically. A closer look at the definition of the
Employeeclass suggests that we need a way to turn off dynamic binding, since functions likegetName()that only retrieve attributes aren't likely to vary. The only method that seems to need it is in factcomputePay(). To turn off dynamic method binding, use thefinalkeyword, like this:public final String getName() { return name; } public final double getRate() { return rate; } public final void recordTime(double time) { timeWorked = time; }Final methods are just that - "final"; you cannot override them in subclasses. You can also declare a class final, which means that it can't be a superclass.
With a little imagination applied to this example you can appreciate the power of polymorphism. Consider a more complex payroll system in a separate module from the code that builds a heterogeneous collection of
Employeeobjects. The payroll functions only concern themselves withEmployeeobjects, and yet the right things happen because the methods called operate on the objects' dynamic type. If there is a change tocomputePay(), the payroll code does not change. Furthermore, if you add a new type of employee, the payroll code still doesn't change! Dynamic binding takes care of invoking the right method. This perfect separation of interface and implementation known as polymorphism makes for clairvoyant code, as it were, because you can process objects of a type that doesn't even exist yet!It is also possible to downcast, that is, to cast from a general type to a more specific type, but only if an object's dynamic type allows it. In Figure 4, for example, the dynamic type of
eisEmployee, so I can't use it as aSalariedEmployee. Theinstanceofoperator is similar to C++'sdynamic_cast, and returns true if the dynamic type of the first operand is identical to or a subclass of the type specified in the second operand. If you try to downcast when you shouldn't you get aClassCastException.One caution in using inheritance is to be mindful of the access you give methods in a subclass. When you override an inherited method in a subclass you can increase its access (e.g., from private to public) but not decrease it. Otherwise you would destroy the ability of a subclass object to behave like its superclass. In other words, a subclass can provide more functionality, but not less. Likewise, a subclass can require less of its method's parameters, but not more. (For the more academically-minded reader, the last two sentences describe
covarianceandcontravariance, respectively). In all ways a subclass instance should be substitutable for a superclass one.The Mother of All Classes
When you define a class that doesn't extend another, your new class implicitly inherits from a class name
Object, so all classes in Java are actually in one huge hierarchy withObjectas the root. TheObjectclass has a method namedtoString(), which executes whenever an object needs to be represented as a string, like when you pass it toSystem.out.println(). Unless you overridetoString()in your class,Object.toString()gives you a string consisting of the class name, the @-sign, and the hash code of the object (a unique, system-generated integer), as the following example illustrates.class MyClass {} class ToStringTest { public static void main(String[] args) { MyClass m = new MyClass(); System.out.println(m); } } /* Output: MyClass@719aea8b */Since
toString()is not a final method, however, you can override it to render a string of your choice, as inclass MyClass { public String toString() { return "MyClassObject"; } }The Java collection classes act as generic containers by holding instances of
Object. The program in Figure 5 uses anArrayList, a vector-like collection, to store some instances ofInteger. In the call to add, theIntegerobject upcasts to anObject, since that is whatArrayList.add()expects, and since everything, includingInteger, is a subclass ofObject. But to get anIntegerback you need to explicitly cast the result of the call toArrayList.get(). If you try to cast to the wrong type you'll get aClassCastException.Abstract Classes
Sometimes a class at the top of a hierarchy represents a general concept that only exists for the purpose of unifying its subclasses. You can look at
Employeein this way, in fact. That is, why is an hourly employee more of an employee than a salaried employee? Could I have just as easily usedSalariedEmployeeas the superclass? Of course. It makes more sense, then, to have a generalEmployeeclass and to derive bothSalariedEmployeeandHourlyEmployeeand whatever else from it (see Figure 6). Because there is no need to actually instantiate anyEmployeeobjects, it's called an abstract class. In Figure 7 I have redefined theEmployeeclass with theabstractkeyword, and have included everything that applies to all typesEmployeeobjects, including a declaration ofcomputePay()as an abstract method. BecauseEmployeeis an abstract class, the Java compiler will not allow it to be the target of the new operator, so you can't have anyEmployeeobjects. BecausecomputePay()is abstract, a subclass, in order to be concrete (i.e., instantiable), must override it and provide an implementation.SalariedEmployeealready fills this requirement, so its code doesn't change. The code forHourlyEmployeeis in Figure 8, and the test program is in Figure 9. Any class with an abstract method is abstract, and must be so declared, although declaring a class abstract is sufficient to prohibit objects thereof, whether it has an abstract method or not. Abstract methods must not have a body, and any method without a body must be declared abstract.Java vs. C++
As in C++, in Java you cannot access private superclass members in a subclass. I used package access in the employee classes, but if you wanted to access
Employeemembers in a subclass defined in another package, you would have to declare those members protected.A common error made by C++ novices is to override a function without making it virtual. The problem doesn't appear until you try to access objects through a pointer or reference, and the lack of dynamic binding may go undetected for some time. Java finesses this situation by making all methods virtual by default, and should you turn off dynamic binding by applying the
finalqualifier, you can no longer override that function. It is also a nice feature to be able to explicitly declare a class abstract, instead of requiring the presence of a pure virtual function to do it.Another "gotcha" for C++ novices is to confuse overloading with overriding. In the following C++ code, for example, the function
B::fis not found when searching for a function to match the expressiond.f().struct B { virtual void f() {} }; struct D : B { void f(int) {} }; int main() { D d; d.f(); // error d.f(1); // OK }A function in a derived class hides all functions of the same name in the base class, regardless of signature, because C++ follows the following scheme for name lookup:
- Find a scope that contains the name of the function, starting in the current class, then going to any enclosing scopes if necessary.
- Once the name is found, do overload resolution to find a matching function.
- If a function was found, see if its access allows you to use it in the current context.
Since there is a name
fin D, step 1 is satisfied but step 2 fails, since there is no function with signaturef()in D. Java, on the other hand, allows overloading and overriding to coexist (see Figure 10).Another feature missing in Java is different types of inheritance. The
extendskeyword is all you get, and that means the same thing as public inheritance in C++ (i.e., "is-a" inheritance). It is nice to sometimes to use private inheritance for implementation reuse in C++, but the need for it is rare and using an embedded object instead is a suitable workaround. I have never seen a need for protected inheritance in C++. (I think it's just there for theoretical reasons).Perhaps the biggest difference between Java and C++ with respect to objects is that Java does not support multiple inheritance - a class can only extend one class. At first blush this may seem quite restrictive, but in truth Java doesn't support multiple implementation inheritance, but it does support multiple inheritance of interfaces. If you've ever had to plumb the depths of virtual base classes (in my opinion the darkest corner of C++), you might appreciate this restriction. In next month's article I'll cover interfaces in Java, but suffice it to say for now that a class in Java can implement any number of interfaces, which is the most common use of multiple inheritance in C++ anyway. Java attempts to provide support for object-oriented programming with a balance of safety, simplicity, and utility. It does a pretty good job.
Figure 1: A Simple Employee Class
class Employee { String name; double rate; double timeWorked; public Employee(String name, double rate) { this.rate = rate; this.name = name; } public String getName() { return name; } public double getRate() { return rate; } public void recordTime(double time) { timeWorked = time; } public double computePay() { if (timeWorked > 40.0) return 40.0*rate + (timeWorked - 40.0)*rate*1.5; else return timeWorked * rate; } }
Figure 2: Illustrates upcasting and dynamic binding
class EmployeeTest { private static Employee[] emps; static void pay(Employee e) { System.out.println(e.getName() + " gets " + e.computePay()); } public static void doPayroll() { for (int i = 0; i < emps.length; ++i) pay(emps[i]); } public static void main(String[] args) { Employee e1 = new Employee("John Hourly", 16.50); e1.recordTime(52.0); SalariedEmployee e2 = new SalariedEmployee("Jane Salaried", 1125.0); e2.recordTime(1.0); emps = new Employee[2]; emps[0] = e1; emps[1] = e2; doPayroll(); } } /* Output: John Hourly gets 957.0 Jane Salaried gets 1125.0 */
Figure 4: Illustrates instanceof and Downcasting
class DownCastTest { public static void main(String[] args) { Employee e = new Employee("John Hourly", 16.50); // The following is true: System.out.println(e instanceof Employee); // The following is false: System.out.println(e instanceof SalariedEmployee); // An invalid cast: SalariedEmployee s = (SalariedEmployee) e; } }
Figure 5: Illustrates Upcasting and Downcasting with a Collection
import java.util.*; class ArrayListTest { public static void main(String[] args) { // Populate collection: ArrayList a = new ArrayList(); a.add(new Integer(1)); a.add(new Integer(2)); for (int i = 0; i < a.size(); ++i) { Integer n = (Integer) a.get(i); System.out.println(n); } } } /* Output: 1 2 */
Figure 7: An Abstract Employee Class
abstract class Employee { String name; double rate; double timeWorked; public Employee(String name, double rate) { this.rate = rate; this.name = name; } public final String getName() { return name; } public final double getRate() { return rate; } public final void recordTime(double time) { timeWorked = time; } public abstract double computePay(); }
Figure 8: The HourlyEmployee Class
class HourlyEmployee extends Employee { public HourlyEmployee(String name, double rate) { super(name, rate); } public double computePay() { if (timeWorked > 40.0) return 40.0*rate + (timeWorked - 40.0) * rate*1.5; else return timeWorked * rate; } }
Figure 9: Testing the Employee Hierarchy
class EmployeeTest { private static Employee[] emps; static void pay(Employee e) { System.out.println(e.getName() + " gets " + e.computePay()); } public static void doPayroll() { for (int i = 0; i < emps.length; ++i) pay(emps[i]); } public static void main(String[] args) { emps = new Employee[2]; emps[0] = new HourlyEmployee("John Hourly", 16.50); emps[0].recordTime(52.0); emps[1] = new SalariedEmployee("Jane Salaried", 1125.0); emps[1].recordTime(1.0); doPayroll(); } }
Figure 10: Shows both Overriding and Overloading
class B { public void f() { System.out.println("B.f()"); } } class D extends B { public void f(int i) { System.out.println("D.f(" + i + ")"); } } class ScopeTest { public static void main(String[] args) { D d = new D(); d.f(); // B.f d.f(1); // D.f } } /* Output: B.f D.f */