Today we look at how to pull out commonality between two different classes, and to use the tools of inheritance to naturally architect their types.
Consider the following two classes, which might be used to write programs that process information on the yard. They might be used in the same program, or perhaps in different programs.
import java.util.*;
public class Mid {
private String first, last, uname;
private int rank, co;
public Mid(String f, String l, String u, int r, int c) {
first = f;
last = l;
uname = u;
rank = r;
co = c;
}
public boolean before(Mid m) {
if( !last.equals(m.last) )
return last.compareTo(m.last) < 0;
if( !first.equals(m.first) )
return first.compareTo(m.first) < 0;
return uname.compareTo(m.uname) < 0;
}
public String email() {
return uname + "@usna.edu";
}
public String fullName() {
return title() + " " + first + " " + last;
}
public String title() {
return "Midshipman " + rank + "/C";
}
public String getCo() {
return co + ending(co) + " Co";
}
public static Mid read(Scanner sc) {
return new Mid(sc.next(), sc.next(), sc.next(), sc.nextInt(),
sc.nextInt());
}
public String toString() {
return fullName() + ", " + getCo();
}
private String ending(int i) {
String[] e = {
"th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"
};
return (i / 10 == 1) ? "th" : e[i % 10];
}
}
import java.util.*;
public class Prof {
private String first, last, uname, grade, dept;
public Prof(String f, String l, String u, String g, String d) {
first = f;
last = l;
grade = g;
uname = u;
dept = d;
}
public boolean before(Prof m) {
if( !last.equals(m.last) )
return last.compareTo(m.last) < 0;
if( !first.equals(m.first) )
return first.compareTo(m.first) < 0;
return uname.compareTo(m.uname) < 0;
}
public String email() {
return uname + "@usna.edu";
}
public String fullName() {
return title() + " " + first + " " + last;
}
public String title() {
if (grade.equals("full")) {
return "Professor";
} else if (grade.equals("assoc")) {
return "Associate Professor";
} else {
return "Assistant Professor";
}
}
public String getDept() {
return dept;
}
public static Prof read(Scanner sc) {
return new Prof(sc.next(), sc.next(), sc.next(),
sc.next(), sc.nextLine());
}
public String toString() {
return fullName() + ", " + getDept();
}
}
Note: here are two classes for testing out Mid and Prof: TestMid and TestProf.
If you look at these two classes, you immediately see that there's a lot of duplication between them.
Duplication of code is a sin! You should have learned that already in IC210, where you probably got yelled at for copy&paste-ing the same code in many spots in your program rather than pulling that code into a single function that you can call in multiple places. A benefit of OOP is that it gives us yet more powerful ways of avoiding code duplication. For example, in Lab 6 when you wanted your colored dot to turn left, you used inheritance instead of copying&paste-ing the Thing.java source code and then making a few changes, which would have left lots of duplicate code. By the way, duplication of data is just as much of a sin!
Hopefully by now you have enough of an Object-Oriented mindset that you immediately think of inheritance. However, we can't really derive Mid from Prof, because Prof has some things (like "department") that Mid doesn't. More philosophically, we don't have an "is-a" relationship. It's not true that a Mid "is-a" Prof. We also can't derive Prof from Mid for all the same reasons. We appear to be stuck.
The solution to the above problem is to pull out of both Mid and Prof what they have in common, and make that a base class that both Mid and Prof can extend. I think we can all agree that Mids and Profs are both examples of people, so we will call this new common base class "Person".
public class Person {
private String first, last, uname;
public Person(String f, String l, String u) {
first = f;
last = l;
uname = u;
}
public boolean before(Person m) {
if( !last.equals(m.last) ) {
return last.compareTo(m.last) < 0;
}
if( !first.equals(m.first) ) {
return first.compareTo(m.first) < 0;
}
return uname.compareTo(m.uname) < 0;
}
public String email() {
return uname + "@usna.edu";
}
public String fullName() {
return title() + " " + first + " " + last;
}
public String toString() {
return fullName();
}
public String title() {
return "Mr/Ms";
}
}
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.
import java.util.*;
public class Mid extends Person {
private int rank, co;
public Mid(String f, String l, String u, int r, int c) {
super(f, l, u);
rank = r;
co = c;
}
public String title() {
return "Midshipman " + rank + "/C";
}
public String getCo() {
return co + ending(co) + " Co";
}
public static Mid read(Scanner sc) {
return new Mid(sc.next(), sc.next(), sc.next(), sc.nextInt(),
sc.nextInt());
}
public String toString() {
return fullName() + ", " + getCo();
}
private String ending(int i) {
String[] e = {
"th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"
};
return (i / 10 == 1) ? "th" : e[i % 10];
}
}
import java.util.*;
public class Prof extends Person {
private String grade, dept;
public Prof(String f, String l, String u, String g, String d) {
super(f, l, u);
grade = g;
dept = d;
}
public String title() {
if( grade.equals("full") ) {
return "Professor";
} else if( grade.equals("assoc") ) {
return "Associate Professor";
} else {
return "Assistant Professor";
}
}
public String getDept() {
return dept;
}
public static Prof read(Scanner sc) {
return new Prof(sc.next(), sc.next(), sc.next(),
sc.next(), sc.nextLine());
}
public String toString() {
return fullName() + ", " + getDept();
}
}
Question: So what, other than the happy feeling of being object-oriented, 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:
import java.util.*;
public class TestPerson {
public static void main(String[] args) {
Person[] A = new Person[100];
int n = 0;
// Read from the terminal.
Scanner sc = new Scanner(System.in);
while( !sc.hasNext("done") )
A[n++] = sc.next().equals("mid") ? Mid.read(sc) : Prof.read(sc);
// Sort (using selection sort).
for( int len = n; len > 1; len-- ) {
int imax = 0;
for( int i = 1; i < len; i++ ) {
if( A[imax].before(A[i]) )
imax = i;
}
Person p = A[len-1];
A[len-1] = A[imax];
A[imax] = p;
}
// print
for( int i = 0; i < n; i++ )
System.out.println(A[i]);
}
}
~/$ 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
The classes Person, Mid and Prof form what's called a class hierarchy, with Person at the top of the hierarchy and Mid and Prof directly underneath the class Person. There is a natural graphical depiction of the relationship between Person, Mid and Prof:
Person / \ / \ Mid ProfOften 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 MusicMidThis would mean that Mid and Prof both
extend
Person,
and that VarAthMid and MusicMid both extend
Mid.
Casting is converting a value from one type to another. In the case of Java, casting
objects (as opposed to primitive values) is really just reinterpreting references. If you
have reference 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!) If you don't want your program to crash when the cast fails, you can 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! In fact, there is a mechanism in Java that comes in handy when necessary called instanceof:
Mid m = null;
if( p instanceof Mid )
m = (Mid)p;
This is a little better, but should still feel a little dirty. There are scenarios where this is useful, though, and perhaps you can think of some!
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:
import java.util.*;
public class TestPerson {
public static void main(String[] args) {
// read
Scanner sc = new Scanner(System.in);
Person[] A = new Person[100];
int n = 0;
while (!sc.hasNext("done")) {
A[n++] = sc.next().equals("mid") ? Mid.read(sc) : Prof.read(sc);
}
// sort (using selection sort)
for (int len = n; len > 1; len--) {
int imax = 0;
for (int i = 1; i < len; i++) {
if (!A[i].before(A[imax])) {
imax = i;
}
}
Person p = A[len - 1];
A[len - 1] = A[imax];
A[imax] = p;
}
// print
for (int i = 0; i < n; i++) {
String aff = "";
if( A[i] instanceof Mid ) {
Mid m = (Mid)A[i];
aff = m.getCo();
}
else if( A[i] instanceof Prof ) {
Prof p = (Prof)A[i];
aff = p.getDept();
}
System.out.println(A[i].fullName() + " - affiliated with " + aff);
}
}
}
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.