Polymorphism Revisited


Recall from Chapter 4 that polymorphism means "many forms," implying "one name, many forms." In Chapter 4, you saw polymorphism involving method overloading (reusing a method name within a class). The previous section presented overriding, which is polymorphism of a different kind: name reuse in an inheritance hierarchy.

This section will present a powerful technique involving overriding polymorphism. Let's begin by exploring the difference between the class of an object and the type of a reference.

The only way to create an object is to call a constructor. Not surprisingly, an object's class is the class whose constructor was called when the object was created. So new Officer(50, 3); creates an object whose class is Officer.

A reference is not an object. You have already seen that a reference is a configuration of 32 bits that uniquely identifies an object. References, not objects, are passed as method arguments. References, not objects, are declared as variables. The type of a reference variable is the type that appears in the variable's declaration. So Worker dagwood; declares that dagwood is a reference of type Worker.

So far, this should be glaringly obvious. Almost always, when a reference points to an object, the type of the reference is the same as the class of the object. This happens, for example, in the line Worker dagwood = new Worker(22222.22f);.

But sometimes the reference type and the object class are not the same. Let's introduce this idea with an analogy to something you already know about. In Chapter 3, "Operations," you learned that you can assign a numeric value to a variable whose type is the same as, or wider than, the type of the numeric value. So you can assign a byte to a float, or an int to a double, as shown here:

byte b = 12; float f = b; int i = 54321; double d = i;

The new type (on the lhs of the assignment) must have enough capacity to encompass the value. Any extra capacity is no problem. The same rule holds when you're passing an argument to a method. If the method expects an argument of a certain type, you can pass data of any type, provided the type declared in the method is the same as, or wider than, the type you actually pass.

A similar principle applies when you're assigning references. Consider this code:

newRef = oldRef; 

It is legal for newRef and oldRef to have different types, as long as the type of newRef is above the type of oldRef in the inheritance hierarchy. So the following is legal:

1.  Worker dagwood; 2.  Employee emp; 3. 4.  dagwood = new Worker(22222.22f); 5.  emp = dagwood;

Here you have one object with two references. Clearly the class of the object is Worker, because it is Worker's constructor that is called on line 4. On line 5, the type of reference emp is Employee, which is the immediate superclass of reference dagwood. Thus, the rule is obeyed, and line 5 is legal. You could also pass dagwood as an argument to a method that declared it took an Employee argument.

So now you know that the type of a reference can be different from the class of the object the reference points to. This puts a subtle but very important restriction on the Java compiler. You and I can look at lines 4 and 5 and say to ourselves, I know emp has type Employee, but really it points to a Worker. We can do that, but the compiler can't.

This isn't a shortcoming on the part of the people who wrote the compiler. In fact, the developers of the Java compiler are some of the smartest programmers in the world. But there are fundamental theoretical limits on what a compiler can do. No car designer, no matter how brilliant, can make a race car that goes faster than light. Similarly, nobody can create a compiler that, in the general case, can look at a reference and know what the class of the reference's target will be when the code is executed.

To put this more succinctly: At compile time, the compiler only knows the types of references. It does not know the classes of objects. This means that a reference's type dictates the variables and methods you can access via that reference. You may only access variables and methods that are the same type as the reference. To return to our example, the reference emp has type Employee, so by using emp, you can read and write variables and call methods of Employee. (The data and methods can be implemented in the Employee class, or they can be inherited from a superclass.) Using the reference dagwood (whose type is Worker), you can access the data and methods (whether directly implemented or inherited) of Worker. This is shown in Table 8.1:

Table 8.1: References, Variables, and Methods

Variables via emp

Variables via dagwood

Methods via emp

methods via dagwood

Id

Id

printCheck()

printCheck()

salary

salary

dumpSalary()

getsOvertime

Given the information in Table 8.1, the previous code example might be baffling. Here is the code:

1.  Worker dagwood; 2.  Employee emp; 3. 4.  dagwood = new Worker(22222.22f); 5.  emp = dagwood;

The table clearly shows that the reference dagwood gives you access to all the data and variables you can get to via emp, and more. Even though line 5 is legal, why would you ever do it? Line 5 just trades a perfectly good reference for one that is less powerful. There must be some compensating benefit to doing this, or nobody in their right mind would ever want to.

In fact, there is a very valuable compensating benefit, as you will see in the next section.

Inheritance Polymorphism

Let's continue our code example just a bit further. No doubt, there must be a piece of code somewhere in the program that periodically prints a paycheck for everybody who works at the company. Let's suppose there's a class called Paymaster that knows who all the employees are. Paymaster might look something like this:

public class Paymaster {   Worker[]    workers;   Manager[]   managers;   Officer[]   officers;   void payEveryone()   {     for (int i=0; i<workers.length; i++)     {       Worker wor = workers[i];       wor.printCheck();     }     for (int i=0; i<managers.length; i++)     {       Manager man = managers[i];       man.printCheck();     }     for (int i=0; i<workers.length; i++)     {       Officer off = officers[i];       off.printCheck();     }   } } 

In reality, the Paymaster class would need a lot more code, including a constructor to set up the three arrays. In fact, there would be a lot more arrays. Companies don't just have workers, managers, and officers. They have presidents, vice presidents, directors, part-timers, and possibly many others. There could be lots of categories of people who need to get paid, and if there had to be one array for each category, that would make for a lot of arrays.

Just to hammer the point home, let's suppose there are classes called President, VP, Director, and PartTimer, each of which extends Employee. We won't show the code for these classes, but here is the monster that Paymaster has become:

public class Paymaster {   Worker[]    workers;   Manager[]   managers;   Officer[]   officers;   President   prez;   // No array: there's only 1 president   VP[]      vps;   Director[]  directors;   PartTimer[] partTimers;   void payEveryone()   {     for (int i=0; i<workers.length; i++)     {       Worker wor = workers[i];       wor.printCheck();     }     for (int i=0; i<managers.length; i++)     {       Manager man = managers[i];       man.printCheck();     }     for (int i=0; i<workers.length; i++)     {       Officer off = officers[i];       off.printCheck();     }     prez.printCheck();     for (int i=0; i<vps.length; i++)     {       VP veep = vps[i];       veep.printCheck();     }     for (int i=0; i< directors.length; i++)     {       Director dir = directors[i];       dir.printCheck();     }     for (int i=0; i<partTimers.length; i++)     {       PartTimer pt = partTimers[i];       pt.printCheck();     }   } }

Let's see how much you can simplify this code, using inheritance polymorphism. The first thing to do is eliminate all those arrays and replace them with a single array, called employees:

public class Paymaster {   Employee[]    employees;   . . .

The components of the new array aren't really employees. That is, employees is an array of references whose types really are Employee, but the classes of the objects pointed to by those references are really Worker, Manager, Officer, and so on. The array is initialized by a lot of code along the following lines, which might appear in Paymaster's constructor:

. . . Worker       dagwood; Manager      julius; President    preston; Director     deirdre, dirwood; . . . employees[1154] = dagwood; // Employee <- Worker employees[1155] = julius;  // Employee <- Manager employees[1156] = preston; // Employee <- President employees[1157] = deirdre; // Employee <- Director employees[1158] = dirwood; // Employee <- Director . . .

The employees array is a cluster of references, all of type Employee. Each of the 5 commented assignment lines stores a reference in a component of the array, and not one of those references is actually of type Employee. That's okay. The rhs references are all of types that are subclasses of Employee, so the "up-the-inheritance-hierarchy" assignment rule is obeyed.

Now let's return to Paymaster's payEveryone() method. Here is all you have to do:

  void payEveryone()   {     for (int i=0; i<employees.length; i++)     {       Employee emp = employees[i];       emp.printCheck();     }   }

That's all! All the references to all the people are now living peacefully together in one diverse community... er, array of references, where the classes of the objects pointed to are unknown and mixed. But you do know that every class is a subclass of Employee (or is Employee itself). So every object has a printCheck() method. This method might be the version inherited from Employee, or it might be an overriding version.

What happens when the payEveryone() loop pays an officer? Recall that the Officer class overrides printCheck() to use a fancy printer with fancy paper. You have a reference (some component of the employees array) of type Employee, pointing to an object of class Officer. Each has its own version of printCheck(). Which one wins?

The answer, and this is crucially important, is that the type of the reference is ignored. The class of the object being called determines which version of an overridden method will be called. So in this example, all the officers will get their checks printed in fancy paper, and any other classes that override printCheck() will have the appropriate version called.

In case this is overwhelming you, let's look at a very simple example that illustrates the same principle:

public class FlyingMachine {   void whoAreYou()   {     System.out.println("I am a flying machine.");   } }

Subclass FlyingMachine like this:

public class Helicopter extends FlyingMachine {   void whoAreYou()   {     System.out.println("I am a helicopter.");   }   public static void main(String[] args)   {     FlyingMachine fm = new Helicopter();     fm.whoAreYou();   } } 

When the application runs, the output is "I am a helicopter." This proves that the class of the object, not the type of the reference, determines the method version that gets called.




Ground-Up Java
Ground-Up Java
ISBN: 0782141900
EAN: 2147483647
Year: 2005
Pages: 157
Authors: Philip Heller

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net