Note: here are two classes for testing out Mid and Prof: TestMid and TestProf, and here's a nice handout version of the above code (print landscape).
ACTIVITY
In pairs or groups of three, go through the two classes and
circle any fields or methods that are duplicates.
Duplication of code is, of course, a mortal sin, so we should
find a way to refactor that gets around this problem!
Also:
Put a box around any prototypes
(or signatures in Java parlance) that are the same in
both classes, even if the implementations are different.
Notice that we had to manufacture a new "title()" function for class Person. You see, both Mid and Prof have "title()" functions, but both are specific to one class or another. However, the "fullName()" method requires calling the "title()" function, so we need to have a "title()" function in the class Person. Otherwise, we literally just pulled the common methods and fields out of the two original classes. At any rate, with the commonality factored out, we can redefine Mid and Prof as classes that extend Person.
Question: So what, other than the happy feeling of being object-orientedy, have we gained? First of all, don't underestimate that happy feeling. Secondly, we have many concrete benefits. If we want to also create a class for coaches, we can now do that with a minimum of extra work. More importantly, however, is that we can now deal nicely with collections that mix Mids and Profs. Why? Because we can have an array (or list) of Person references, each of which can point equally well to both Mids and Profs, and all of the methods that are defined for both Mids and Profs can be called on the array elements and polymorphism will take care that the function that's appropriate for the actual type of the object the array element points to is called. Here's a simple example of such a program:
| TestPerson.java | Sample Run |
~/$ java TestPerson
mid Janet Jackson m189845 3 24
prof Red Foo redfoo assoc Computer Science
prof Taylor Swift tswivel assist Chemistry
mid Chet Atkins m162343 1 27
prof Alex Chilton achilt full Mathematics
mid Meghan Trainor m190377 4 17
done
Midshipman 1/C Chet Atkins, 27th Co
Professor Alex Chilton, Mathematics
Associate Professor Red Foo, Computer Science
Midshipman 3/C Janet Jackson, 24th Co
Assistant Professor Taylor Swift, Chemistry
Midshipman 4/C Meghan Trainor, 17th Co
|
Person
/ \
/ \
Mid Prof
Often in designing an object oriented program we start off by
sketching out one or more class hierarchies that we'll want to
create. That's how important and fundamental class hierarchies
are to object oriented programming. These hierarchies can get
more complex too. If the program had to have separate
facilities for dealing with Mids that are varsity atheletes and
also for mids in musical groups (e.g. glee club or drum and
bugle) the hierarchy might grow like this:
Person
/ \
/ \
Mid Prof
/ \
/ \
VarAthMid MusicMid
This would mean that Mid and Prof both extend
Person, and that VarAthMid and MusicMid both extend
Mid.
Mid m that's been assigned an object,
you are free to "cast" m to a reference to type Person with the
expression (Person)m. Not that this does much,
since m literally "is-a" Person by virtue of
inheritance. The other direction is scarier. If I
have Person p can I cast it to a Mid? The answer
is "yes" (you do it with (Mid)p, of course), but
it's a qualified "yes". You see, the cast can fail if the
object that p points to is not actually a Mid
(or something derived from Mid); if,
for example, it is a Prof instead. (Clearly you cannot make a
Mid out of a Prof!) Java forces you to deal with the
possibility of this failure using its "exceptions" mechanism,
which we haven't covered yet. So, for today take it on faith
that you do the conversion like this
Mid m = null;
try { m = (Mid)p; } catch(Exception e) { }
... recognizing that the cast may fail, with the result
that m is not assigned a new value so that, in this
example, it would stay null. In some sense, this gives you a
way to check the type of object p points to, and do different
things according to the result. As it turns out, this is seldom
a "good" object oriented programming move!
Continuing with our Person/Mid/Prof example, let's suppose that we want to print out each person's full name and their "affiliation", which will be "company" for Mids and "department" for Profs. We could handle this by casting our Person objects to Mid or Prof as appropriate, and using their getCo() and getDept() methods to get the affiliation. Here's an example of how:
Although this works, it is not good OOP practice. As a rule, objects should act in the manner appropriate to themselves — which is accomplished through polymorphism. To let the objects "act for themselves", however, we have to have a method in the base class that the various derived classes can override in order to "act for themselves". In fact, we already have that behavior with the "fullName()" method. So, to do the same for "affiliation", we need to put an "affiliation" method in the base class "Person". If we do that, and override the affiliation() method in Mid and Prof, the print loop becomes a simple, beautiful (and very OOPy):
// print
for(int i = 0; i < n; i++)
{
System.out.println(A[i].fullName() + " - affiliated with " + A[i].affiliation());
}
Almost always, if you catch yourself wanting to figure out the
actual type of an object and to "cast" to that actual type, it
is an indication that you need to add one or more methods to the
base class and override them appropriately in the derived classes.
Important!
the affiliation() method must be added to
the base class (Person) as well as the two derived
classes because, and this is the important part, the type of the
reference determines what method signatures we are allowed to call, even
though the type of the object the reference points to determines
which actual version of the method with that signature will get
called. So if affiliation() were not in the base
class, we would not be allowed to make the call
A[i].affiliation().
This makes sense if you think about it, because the only method
signatures
that are guaranteed to be available for objects referred to by
a Person reference are the methods signatures in
class Person. This means that in Java, we are
guaranteed to never have a "no such method" error occur at
run-time.