Table of Contents
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 use 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 refers to 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.
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’s. 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 opject 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. When you start
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); //<--
}
}
Some Vocabulary #
We need to make sure we’re on the same page with our vocabulary so that we can talk meaningfully about what happens in programs. Some concepts have one name in Java, but a different name in the larger world of programming languages. I’ll use the generally accepted terms here, but point out what the Java-esque equivalents would be.
-
call site – the location in code that specifies that a function should be called. The full term is function call site or, in Java parlance, method call site. A call site is part of the program code, whether it be source code or compiled code.
-
function call – the actions taken by a running program to execute a function. Of course in Java parlance this would be a method call. A function call is part of the execution of a program, i.e. it is something that happens at runtime for a program.
Note 1: we often say “function call” when we really mean “call site”, but that’s being sloppy … though hopefully we are only sloppy in situations for which the distinction between “function call” and “call site” are not important.
Note 2: it’s often the case that one call site can have many associated function calls — for example, when a call site is inside a loop. For example:
double[] a = {1.3,1.6,1.8}; double sum = 0.0; for( int i = 0; i < a.length; i++ ) sum += Math.sqrt(a[i]);
There is only one call site in this code, but when the code is executed, three function calls will result.
-
static function call / call site — A static call site is one for which it can be determined before runtime exactly which function will be executed when the program executes the call. A static function call is simply a call from a static call site. All function calls / call sites in C are static. In Java, call sites for static methods are always static — which makes some sense.
-
polymorphic function call / call site — a call site is polymorphic if the question of which actual function to execute can only be determined at runtime; and in fact, the function call may change from call-to-call associated with the call site within a single run of a program. To recap:
Foo x; // . // . <- x gets assigned to point to an object // . x.func(arg1,arg2,...,argk);
Then to determine what actual function will get executed you need to look not only at the types of arg1,…,argk in order to take function overloading into account (here you’ll be looking at their static types, e.g. the types of the references rather than the objects they point to), but you also need to look at the actual, most specific type of the object x points to — which may be of type Foo or may be of a type derived from Foo.
-
polymorphic on — In a polymorophic function call in Java, it is the type of the implicit parameter, the this pointer, that determines which actual function gets called. So we say something like “the call is polymorphic on the implicit argument”. There are languages that are polymorphic on the other arguments, but they are unusual.
Example of Using Polymorphism #
Hopefully you’ve got the idea that polymorphism is a really cool mechanism. But what should we do with it? It’s a hammer, what’s the nail? Well, polymorphism solves the last of the three “problems” that we had with Procedural Programming not fully separating interface from implementation: how to allow multiple implementations for the same interface.
Below is a non-trivial example that shows how polymorphism can be harnnesed to write non-trivial programs with ease.
Basic Expense Report Generator #
Suppose we’re writing a software to to enter expense reports. There are number of different expenses one can enter, for example, a meal, a plane ticket, or various incidentals (like tipping doorman at the fancy hotel you just travelled to!). Our goal is to write a program like the following:
$./java ExpenseReport
Enter dollar amount for lodging (0 for none): 357.89
Enter dollar amount for meals (0 for none): 170.08
Enter dollar amount for incidentals (0 for none): 58.23
$0357.89 lodging
$0170.08 meals
$0058.23 incidentals
---------
$0586.20 total
Your OOP spidey-sense should be tingling! Let’s start with defining an expense class to manage expenses. This is straight forward: we’ll need to categorize the name of the expense and the amount, plus some other setters and getters. We will also want a method to “get” or “ask” for the expense amount.
public class Expense {
private String descrip;
private double amount;
public Expense(String descrip);
public String getDescrip();
public double getAmount();
public void setAmount(double a);
// Get amount from user and set
// amount appropriately.
public void ask(Scanner sc);
}
The actual code for Expense doesn’t matter, we can imagine we only got the class description, not the source code. But, if you’re interesed, you can find the source code by expanding the link below.
Given a set of expenses, next we need a way to generate a report. For that, we’ll use a procedural program (i.e., a static method) that takes an array of Expense
s perform an ask()
on each of them, and then print out the full report.
public class ReportGenerator {
// Query users for amounts of the
// expense categories listed in
// array e, and output expense report.
public static void create(Expense[] e);
}
Again, the actual details of this code are not important, we can assume we simply get the class and not the source. But if you’re interested, you can expand the link below:
At this point it should be clear, with a simple main()
method we can produce the functionality we desire from above.
public class ExpenseReport {
public static void main(String[] args) {
Expense[] e = {
new Expense("lodging"),
new Expense("meals"),
new Expense("incidentals")
};
ReportGenerator.create(e);
}
}
Polymorphic Expense Report Generator #
This example makes use of good OOP but not inheritance nor polymorphism. But let’s consider what happens when we want to add a slightly more complicated expense, that is “per-item.”
A great example of such an expense, is mileage for car travel. You get reimbursed a fix rate per mile (as in it’s a per-item, or per-mile expense.) The current structure of Expense
cannot accommodate a per-item expense, like mileage, but we can expand Expense
to do just that.
public class PerItemExpense extends Expense{
private double rate;
private double item;
public PerItemExpense(String descrip,
String kind,
double rate){
//initialize Expense with per-itme info
super(descrip+ "(at " + rate + "dollers per " + kind +")");
this.rate = ratel
this.kind = kind;
}
public void ask(Scanner sc) {
System.out.print("Enter amount of " + kind + ": ");
setAmount(sc.nextDouble() * rate); //set ammount at rate
}
}
With this new class, let’s consider two things:
PerItemExpense
is-aExpense
in the sense that it inherits fromExpense
and thus can be used anywhere that expense is used.PerItemExpense
has overwritten the functionality ofExpense
but keeps the same API, as in, it’s a different implementation of how expenses are recorded and calculated, but it uses the same interface of callingask()
and encoding values in the description and the amount is retrievable viagetAmmount()
.
What this means, is that we DO NOT have to overwrite any new code in ReportGenerator
to handle this new expense type. It already takes as input an array of Expense
s – PerItemExpense
is an Expense. In the method, it uses ask()
and getAmmount()
to generator a report — if it’s a per-item expense, at the call site, the per-item functionality will be used, and if it’s a normal expense, the normal functionality will be used.
We achieve the Nirvana of OOP in quite a simple non-trivial program. The main()
method only changes like so
public class ExpenseReport {
public static void main(String[] args) {
Expense[] e = {
new Expense("lodging"),
new Expense("meals"),
new Expense("incidentals"),
new PerItemExpense("mileage","miles", 0.31) //Also an Expense
};
ReportGenerator.create(e);//polymorphism will ensure the right
//functionality is achived at the call site
}
}
And now we get the following output
~/$ java ExpenseReport
Enter dollar amount for lodging (0 for none): 357.89
Enter dollar amount for meals (0 for none): 170.08
Enter dollar amount for incidentals (0 for none): 58.23
Enter amount of miles: 302
$0357.89 lodging
$0170.08 meals
$0058.23 incidentals
$0093.62 mileage (at 0.31 dollars per miles)
---------
$0679.82 total
Casting, Polymorphism and instanceof
#
Polymorphism implies that you might have use a more general type (say an Expense
) to reference a more expressive type (say an PerItemExpense
), like in the example above. For example, we know that in ExpenseReport
that if we did the following
Expence a = e[3]; //a is reall a PerItemExpense
We know that a
is a per-item expense because we wrote the code, and we could, technically cast that to a PerItemExpense
if we wanted to.
PerItemExpense a = (PerItemExpense) e[3];
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 PerItemExpense
but from ReportGenerator.create()
’s perspective, it sees everything as an Expense
. So this cast is dangerous and cause runtime errors and will sometimes cause compile time warnings. It’s also not neccesary because the interface for Expense
is consistent with PerItemExpense
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<4;i++){
if( e[i] instanceof PerItemExpense )
//... do something specific to this class
}
While there are some programs where this is unavoidable, 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 stduents (I 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
calls, 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 GWProfessor(/*...*/);
//...
//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 are 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.
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, include 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);
}
}
Leveraging Abstract Class #
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 Alex Doe G22213 adoe 2023
s Taylor Swift G3123565 tswift 2024
s Ryan Reynolds G123052 rrey 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
Doe, Alex (G22213,adoe) -- Class of 2023 -- adoe@gwmail.gwu.edu
Frank-Bolton, Pablo (G0731345,pfrank) -- Computer Science -- pfrank@email.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
Swift, Taylor (G3123565,tswift) -- Class of 2024 -- tswift@gwmail.gwu.edu
OOP Design Activity #
Let’s take a moment and look at a sample program and try and develop a design for a program using our OOP skills of encapsulation, inheritance, and (now!) polymorphism. In particular we are going to consider a family household simulation. There are both regular expenses and payments.
For example, you expect the following inputs to the simulation:
every 14 days start 1/6/2021 income Paycheck
every 1 months start 1/15/2021 expense Mortgage Due
every 1 months start 1/6/2021 expense Cable Due
every 3 months start 3/21/2021 expense Water/Sewer Due
every 3 months start 3/26/2021 income Stock Dividend
every 1 months start 1/23/2021 expense Credit Card Due
every 1 months start 1/11/2021 expense Car Payment Due
every 1 months start 1/2/2021 expense Car Insurance Due
every 1 months start 1/17/2021 expense Cell Phone Due
every 1 days start 1/7/2021 prob 0.143 expense Groceries
every 1 days start 1/1/2021 prob 0.15 expense Dinner Out
every 7 days start 1/3/2021 prob 0.5 expense Breakfast Out
every 4 days start 1/1/2021 prob 0.5 expense Gas
every 1 days start 1/1/2021 prob 0.01 expense Car Repair
every 12 months start 4/15/2021 expense Taxes Due
every 1 months for 12 start 1/16/2021 expense Lazy Boy Layaway Payment Due
Some of these items should be obvious, others require a bit more explanation.
-
every 3 months start 3/26/2016 income Stock Dividend
This indicates that “Stock Dividend” is an event that first occurs on 3/26/2016 and occurs every three months from that point on. So, for example, the next occurence would be 6/26/2016. -
every 14 days start 1/6/2016 income Paycheck
This indicates that “Paycheck” is an event that first occurs on 1/6/2016 and occurs every 14 days from that point on. So, for example, the next occurence would be 1/20/2016. -
every 1 months for 12 start 1/16/2016 expense Lazy Boy Layaway Payment Due
This indicates that “Lazy Boy Layaway Payment Due” is an event that first occurs on 1/16/2016 and occurs monthly from that point on until it has occurred 12 times. So, for example, the next occurence would be 2/16/2016, and the last occurrence would be 12/16/2016. The limit on the number of occurrences only happens with “every X months” events.
And once all that is loaded in, the simulation runs indicating income and expenses. Like below
1/1/2021: - Dinner Out, - Gas
1/2/2021: - Car Insurance Due
1/3/2021: - Breakfast Out
1/5/2021: - Gas
1/6/2021: + Paycheck, - Cable Due
1/7/2021: - Groceries, - Dinner Out
1/9/2021: - Gas
1/10/2021: - Breakfast Out
1/11/2021: - Car Payment Due
1/13/2021: - Groceries
1/15/2021: - Mortgage Due
1/16/2021: - Lazy Boy Layaway Payment Due
1/17/2021: - Cell Phone Due, - Breakfast Out
1/18/2021: - Dinner Out
1/20/2021: + Paycheck
1/21/2021: - Gas
1/23/2021: - Credit Card Due
1/24/2021: - Groceries
1/25/2021: - Gas
1/29/2021: - Gas
2/2/2021: - Car Insurance Due, - Dinner Out
2/3/2021: - Paycheck
2/5/2021: - Dinner Out
2/6/2021: - Cable Due, - Dinner Out, - Gas
2/7/2021: - Breakfast Out
2/8/2021: - Groceries
2/10/2021: - Dinner Out
2/11/2021: - Car Payment Due, - Groceries
2/12/2021: - Groceries, - Dinner Out
2/14/2021: - Breakfast Out
2/15/2021: - Mortgage Due
...
Take a moment to think of a class hierarchy structure that would make sense for this simulator. You can either draw a UML diagram, or write out your classes members and methods (withou implementation). I do the later below, and click REVEAL to see my solution.
Material from this unit was adopted and/or derived from USNA ic211 (spring 2019)