Table of Contents
- Polymorphism and Abstract Classes
- Inheritance: following “is-a” to its logical conclusion
- Polymorphism
- We are all instances of class
Object
- Let's take 20 minutes to go through questions 1-5 on the J3 worksheet.
- Casting, Polymorphism and
instanceof
- Abstract Classes
- Abstract GWPerson Class
- Extending an Abstract GWPerson Class
- Let's take 10 minutes to go through questions 6-7 on the J3 worksheet.
- Leveraging Abstract Classes
- Let's take 25 minutes to go through the rest of the questions on the J3 worksheet.
View all the videos from this unit a single playlist on youtube
Polymorphism and Abstract Classes #
Recall that there are four basic mechanisms / ideas underlying object oriented programming:
- encapsulation
- information hiding
- inheritance
- polymorphism
Encapsulation (bundling together data and the functions that operate on that data) and information hiding (language mechanisms to enforce the separation of interface from implementation) “solved” the first of our three problems with Procedural Programming not giving us full separation of interface from implementation: the “what to do with structs” problem.
We’ve covered both of these over the last two weeks. We’ve also covered inheritance as a mechanism to modify or extend functionality. It solves the second of the three problems with procedural programming not giving us full separation of interface from implementation: modifying or extending functionality without having to access implementation.
In this lesson, we will cover polymorphism, which is the last of the four basic mechanisms/ideas behind object oriented programming. It solves our last “problem”: how to allow multiple implementations for the same interface.
Inheritance: following “is-a” to its logical conclusion #
Let’s return the example from J1 notes of Points
and LabPoints
. Recall the two interfaces and their primary functionality:
public class Point {
private double x, y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public String toString() {
return x + " " + y;
}
}
public class LabPoint extends Point {
private String lab;
public LabPoint(double x, double y, String lab) {
super(x, y);
this.lab = lab;
}
public String toString() {
return super.toString() + " " + lab;
}
}
For starters, let’s consider a simple program involving only Point
objects.
import java.util.*;
public class Ex1 {
public static void main(String[] args) {
Random rand = new Random(System.currentTimeMillis());
Point v = new Point(3, 4);
Point w = new Point(5, 2);
Point u;
if(rand.nextBoolean())
u = v;
else
u = w;
System.out.println(u.toString());
}
}
Hopefully you see that what this program does is instantiate two points [(3,4) and (5,2)], and then randomly choose one of the two and assign reference u
to point to it. You run this program and you might see 3 4
printed out. Run it again, and you might see 5 2
instead. It’s random.
Now, recall that inheritance is supposed to give us an “is-a” relationship, as in “an object of type LabPoint
is-a Point
. If that’s true, then the variable u
should be able to point to a LabPoint
just as much as it can point to a Point
. So let’s try it. The following program, Ex2.java
, is just like the earlier program, except that the second Point
w
we instantiate is actually the LabPoint
(5,2) with label “A”, rather than just the plain-ol’ Point
(5,2). The code compiles, which certainly proves that the compiler is at peace with the prospect of the variable u
referencing a LabPoint
rather than a plain-ol’ Point
.
import java.util.*;
public class Ex2 {
public static void main(String[] args) {
Random rand = new Random(System.currentTimeMillis());
Point v = new Point(3, 4);
LabPoint w = new LabPoint(5, 2, "A");
Point u;
if( rand.nextBoolean() )
u = v;
else
u = w;
System.out.println(u.toString());
}
Now, here’s the $1,000,000 question: when you run this program and the random choice is to set u = w
, what gets printed out? Do I get 5 2
— since u
is declared as a reference to a Point
— or do I get 5 2 A
— since the object u
refers to is actually a LabPoint
? What do you think? The answer is … drum-roll please … 5 2 A
!
~/$ java Ex2
5.0 2.0 A
This is actually pretty amazing. “Why?”, you ask? Because the the compiler didn’t know which function was going to be called! It couldn’t know, because ultimately which actual method gets executed depends (down to the millisecond) on when the user chooses to execute the program. The random number generator is seeded on the current time. Moreover, from one run to the next, without re-compiling, a different method gets executed. This is truly new. You have never before seen code where you couldn’t tell exactly which function/method would execute at a given call site. In this example, it’s only at run-time that we can determine which function to execute. BTW: The only sane way for this to work (and the way it does work) is for the virtual machine, when it comes to the site of the call u.toString()
, to check what type of object u
points to at runtime and allow that to determine which version of toString()
gets executed.
A call site like this is called polymorphic. The word means “many shapes” and it’s trying to get at the idea that many different functions may result from a single call site. Another common term to refer to a call like this is as a dynamic function call. A function call for which the actual function to be executed can be determined at compile time is referred to as static, because it is “unchanging” from run-to-run of the program, whereas a call for which the question of which actual function to execute can only be determined as the program executes is called dynamic, because it changes from run-to-run or even from call-to-call within a single program execution.
Note that polymorphism is a concept applied both at compile time, and at runtime. When compiling, the compiler allows you to set a reference of a parent class equal to a child class object, due to the inheritance relationship the compiler is aware of. At runtime, the runtime-type of the object is looked up in memory to achive dynamic binding of the correct method (potentially of the child class). You’ll see some examples in the worksheet where we’re asking of how which method is called when polymorphic objects are passed into methods (as opposed to methods being called on a polymorphic object reference) – remember here that this is a compile-time decision.
Polymorphism #
Suppose we have a class Class1
and classes Class2
, Class3
, … ClassK
, that are derived from Class1
, either directly or through a chain of extends
. Suppose further that the base class Class1
defines a method method(Type1,Type2,...,TypeN)
, and that many of the derived classes override this method. If a variable var
is declared as a reference to the base class Class1
, then when the call structure below
Class1 var;
var = new Class3(...);
var.method(arg1,...,argn) // calls Class3's method, not Class1
the actual method that gets executed is based on the type of the object var
currently references not the type in which var
was declared. So, if var
currently points to an object of class ClassI
, then the ClassI
version of the method is the one that actually gets called.
The typing of var
as a Class1
is still correct. But the inheritance says that all sub-types of Class1
are really a Class1
, just with “more stuff.” So there is nothing wrong with using var
to reference a Class2
or a ClassK
.
But, the thing that var
references may not be exactly a Class1
, so when you use the .
operator to access a member or method, you’re really access the more specific version, namely a Class2
or ClassK
.
This polymorphism of var
is extremely powerful! It allows you to write generic interfaces, say for example the toString()
interface for converting an object to a String
in println()
, and then each implementation can be used slightly differently without the caller/designer of the interface having having to know the specific type.
We are all instances of class Object
#
We can see a slightly more interesting example of a polymorphic function call if I let you in on a little secret: all classes are derived from a class called Object
. Specifically, when you define a class without the extends
keyword, that class implicitly “extends” Object
. So the class Point
above is an Object
. Now you can look Object
up in the Java API documentation, and you’ll see that it has some methods. Most notably for us, it has the method “toString()
”. That means you can call .toString()
for any object. So let’s expand our example:
Object
^
|
Point
^
|
LabPoint
And the method println()
takes advantage of this polymorphism by calling the toString()
method of all objects passed to it; the implementation of toString()
depends on the actual type of that instance of the Object
. The fact that Object
has a toString()
method (inherited by all other objects) means this will always succeed in printing something.
Using this knowledge, the polymorphism in the following program really comes to the front:
import java.util.*;
public class Ex3 {
public static void main(String[] args) {
Random rand = new Random(System.currentTimeMillis());
Point v = new Point(3, 4);
LabPoint w = new LabPoint(5, 2, "A");
String x = "I'm a string";
Scanner y = new Scanner(System.in);
Object u;
int i = rand.nextInt(4);
if(i == 0)
u = v;
else if(i == 1)
u = w;
else if(i == 2)
u = x;
else
u = y;
System.out.println(u.toString()); //<--
}
}
Let's take 20 minutes to go through questions 1-5 on the J3 worksheet.
Casting, Polymorphism and instanceof
#
Polymorphism implies that you might have use a more general type (say an Point
) to reference a more expressive type (say an LabPoint
), like in the example above. For example, we know that in LabPoint
that if we did the following
Point p1 = new Point(2, 3);
Point p2 = new Point(1, 5);
LabPoint p3 = new LabPoint(3, 4, "yellow");
Point[] points = {p1, p2, p3};
Point a = points[2]; //a is really a LabPoint
We know that a
is a LabPoint
because we wrote the code, and we could, technically cast that to a LabPoint
if we wanted to.
LabPoint a = (LabPoint) points[2];
LabPoint a = (LabPoint) points[0]; // <---- this will compile, but crash at runtime!
But this is quite dangerous, and should feel a bit ugly (because it is). For starters, we just happen to know that this is a LabPoint
but from points
’ perspective, it sees everything as a Point
. So this cast is dangerous and can cause runtime errors and will sometimes cause compile time warnings, for instance, if you are loopoing through the points
array without knowing what type of object is actually inside of it at each index. It’s also not neccesary because the interface for Point
is consistent with LabPoint
and so we can take advantage of polymorphism.
However, there are times where you will need to know what is the real class-type of an instance object. Java has a method for doing that using instanceof
for(int i=0; i < points.length; i++){
if( points[i] instanceof LabPoint )
//... do something specific to this class
}
While there are some programs where this is unavoidable (we often see this when writing our own equals()
methods), you should typical try to design your code such that it’s fully polymorphic, as in it doesn’t matter which of the specific sub-types the instance is.
Abstract Classes #
Consider that we’re developing a program to represent individuals at GW, simplified for now to consider just faculty and students (we know that there are more than just professors and students at GW). We also need to be able to be able to quickly read in a Person
(e.g., using a Scanner
), sort all the persons we read in in alphabetic order, and retrieve their email addresses.
Before learning about OOP, you may be tempted to program two classes separately a GWStudent
class and a GWFaculty
class, but now we know that we should leverage inheritance to find commonality between professors and students in a super class, perhaps a GWPerson
class. Like so
GWPerson
^ ^
.' '.
| |
GWStudent GWFaculty
This is great! This means we can create array’s of GWPerson
s like so and take advantage of both inheritance and polymorphism in the code we design
GWPerson persons[] = new GWPerson[10];
persons[0] = new GWStudent(/*...*/);
persons[1] = new GWFaculty(/*...*/);
//...
//sort persons using before method.
But let’s think about this for a second; would we ever instantiate a GWPerson
object? No. The whole universe at GW are students and professors. Yes, they are both GWPerson
s but each person must be either a student or professor.
Moreover, there is going to be some functionality that doesn’t make sense to implement in the GWPerson
class because it will have to be overwritten in the sub-class. For example, consider a method email()
which returns the person’s email. This is wholy dependent on if the person is a student or a professor.
Note that a student’s email address is
@gwmail.gwu.edu
and a faculty email address is@email.gwu.edu
.
There is no reason to then implement email()
at all at the GWPerson
level, but instead it must be implemented in the subtype, specific to students or professors. We don’t know the domain of the email until we know which sub-class of GWPerson
(either student of faculty).
While it may be impossible to implement an email()
method, there are other possible methods of GWPerson
that should be common to a GWStudent
and GWFaculty
. For example, a fullname()
method that prints the full name (first and last together) is the same for everyone.
As such, we have a super class, GWPerson
, that is both obvious in that it should be implemented and included in the class hierarchy, but has features that cannot actually be implemented. Additionally, a GWPerson
object should never need to be instantiated … so what really is GWPerson
in this class hierarchy?
Abstract GWPerson Class #
In Java, we have a notion of abstract classes to solve this very problem. It allows the program to describe a class that fits into a class hierarchy for inheritance and code-reuse, e.g., GWPerson
, but should never actually be instantiated because some features/methods cannot exist until properly defined in a sub class.
To indicate that a class or method is “abstract” we use the abstract
keyword to note it when declaring the class, and also in which methods are abstract
. Like below.
Also, recall the protected
modifier (as opposed to public
or private
); a protected
field or method means that an child class of the class “sees” that item, but other classes that are not subclasses cannot. This includes any code outside of the subclass trying to access the item through the subclass object: this is forbidden. protected
also means the item will be visible, in addition to the subclass, to another other class in the parent class’ package.
public abstract class GWPerson { //note GWPerson is abstract !
protected String fname;
protected String lname;
protected String GWID;
protected String uname;
public GWPerson(String fname, String lname, String GWID, String uname);
//these methods can be same for all persons but perhaps overwritten
//to include more specifics in subclasses
public boolean before(Person p){...} //returns True if this "before" p in
//alphabetic ordering
public String fullname() {...}
public String toString() {...}
//this method cannot be defined here and must be implemnted in the subclass
public abstract String email(); //abstract method
}
To see the full implementation, click below.
Declaring a class abstract
means that this class cannot be directly instantiated. For example, the following code will not compile:
GWPerson p = new GWPerson(/*...*/);
That’s because GWPerson
is simply an abstract representation of a person and not fully implemented. Only (non abstract) subclasses of GWPerson
instantiate a person (via polymorphism). For example,
GWPerson p = new GWStudent(/*...*/);
In this case, GWStudent
is-a GWPerson
as it will extends
the GWPerson
class. The same will be the case for GWFaculty
class.
Extending an Abstract GWPerson Class #
When you extend an abstract class, the Java compiler requires you to finish the implementation of that class. In this case, we must overwrite the method email()
. If we were to implement a GWStudent
, we must implement the abstract methods, including the email()
method and the read()
method. If you do not, the compiler provides the following warning:
GWStudent.java:1: error: GWStudent is not abstract and does not override abstract method email() in GWPerson
public class GWStudent extends GWPerson{
^
1 error
Once we do implement email()
, implementing GWStudent
and GWFaculty
are relatively straight forward
import java.util.*;
public class GWStudent extends GWPerson {
protected int classYear; //e.g., class of 2024
public GWStudent(String fname, String lname, String GWID, String uname, int classYear) {
super(fname, lname, GWID, uname);
this.classYear = classYear;
}
//implmenting abstract method
public String email() {
return uname + "@gwmail.gwu.edu";
}
//overwriting super method
public String toString() {
return super.toString() + " -- Class of "+this.classYear;
}
//adding new static method
public static GWStudent read(Scanner sc) {
String fname = sc.next();
String lname = sc.next();
String GWID = sc.next();
String uname = sc.next();
int year = sc.nextInt();
return new GWStudent(fname, lname, GWID, uname, year);
}
}
import java.util.*;
public class GWFaculty extends GWPerson {
protected String dept; //department
public GWFaculty(String fname, String lname, String GWID, String uname, String dept){
super(fname, lname, GWID, uname);
this.dept = dept;
}
public String email() {
return uname + "@email.gwu.edu";
}
public String toString() {
return super.toString() + " -- " + this.dept;
}
//adding new static method
public static GWFaculty read(Scanner sc) {
String fname = sc.next();
String lname = sc.next();
String GWID = sc.next();
String uname = sc.next();
String dept = sc.nextLine();
return new GWFaculty(fname, lname, GWID, uname, dept);
}
}
Let's take 10 minutes to go through questions 6-7 on the J3 worksheet.
Leveraging Abstract Classes #
Abstract classes provide a way to guarantee a subclass of the given class implements all the necessary functionality (i.e., the abstract methods). That means, you can program using the abstract class in a polymorphic way with the expectation that you’ll have those methods available to you.
For example, consider this program that reads in information about students and faculty, sorts them, and prints it out in a nice format.
import java.util.*;
public class ReadPersons {
public static void main(String args[]) {
GWPerson persons[] = new GWPerson[100];
Scanner sc = new Scanner(System.in);
//read in students and faculty
int n = 0;
while(n < 100){
System.out.println("Select:\n"+
"(s) Student\n"+
"(f) Faculty\n"+
"(d) Done");
String opt = sc.next();
if(opt.equals("s"))
persons[n++] = GWStudent.read(sc);
else if(opt.equals("f"))
persons[n++] = GWFaculty.read(sc);
else if(opt.equals("d"))
break;
else{
System.out.println("Unknown option");
continue;
}
System.out.println("");
}
//before a bubble sort
for(int i = 0; i < n; i++) {
for(int j = i + 1; j < n; j++) {
if(!persons[i].before(persons[j])) {
GWPerson tmp = persons[i];
persons[i] = persons[j];
persons[j] = tmp;
}
}
}
System.out.println("");
System.out.println("-----");
System.out.println("");
//print out the info
for(int i = 0; i < n; i++){
System.out.println(persons[i] + " -- " + persons[i].email());
}
}
}
Importantly, note that we are able to refer to persons
array of type GWPerson[]
despite GWPerson
being an abstract class. The only instances of GWPerson
are either GWFaculty
or GWStudent
, and the functionality differences rely on both polymorphism and inheritance.
For example, with the following inputs:
s John Smith G12245 jsmith 2021
s Yang Li G22213 yli 2023
s Taylor Swift G3123565 tswift 2024
s Priyanka Chopra G123052 pchopra 2025
f Adam Aviv G8182309 aaviv Computer Science
f Pablo Frank-Bolton G0731345 pfrank Computer Science
s Harry Styles G120345 styles 2026
d
We would get the following output from this program
Aviv, Adam (G8182309,aaviv) -- Computer Science -- aaviv@email.gwu.edu
Chopra, Priyanka (G123052,pchopra) -- Class of 2025 -- pchopra@gwmail.gwu.edu
Frank-Bolton, Pablo (G0731345,pfrank) -- Computer Science -- pfrank@email.gwu.edu
Li, Yang (G22213,yli) -- Class of 2023 -- yli@gwmail.gwu.edu
Reynolds, Ryan (G123052,rrey) -- Class of 2025 -- rrey@gwmail.gwu.edu
Smith, John (G12245,jsmith) -- Class of 2021 -- jsmith@gwmail.gwu.edu
Styles, Harry (G120345,styles) -- Class of 2026 -- styles@gwmail.gwu.edu
Let's take 25 minutes to go through the rest of the questions on the J3 worksheet.
Material from this unit was adopted and/or derived from USNA ic211 (spring 2019)