🏢 Staff Payroll System

Polymorphism — Real-World Practical Example  ·  Chapter 11

Hello, my students! Now that we understand polymorphism, let's apply it to a real scenario: a university staff payroll system. We have different types of staff — Executives, Employees, Hourly workers, and Volunteers. With polymorphism, one array, one loop, handles them all!
🔄 Polymorphic Array ▶︎ Dynamic Binding ⬇ Casting 🤔 Design Discussion 📈 Degrees of Polymorphism

1 The Real-World Scenario

Imagine you are building a payroll system for a university. The university has four kinds of staff members:

👑 Executive
Senior staff. Receives a fixed monthly salary plus a periodic bonus.
💼 Employee
Regular full-time staff. Receives a fixed monthly pay rate.
🕐 Hourly
Part-time staff. Paid per hour worked: hours × rate.
🤝 Volunteer
Contributes without payment. Receives SAR 0.
💡 The Challenge

All these staff members share some common data (name, address, phone). But each type is paid differently. How can we store them all together and process their payments with a single loop? The answer: Polymorphism!

2 The Class Hierarchy

We design an inheritance hierarchy where StaffMember is the superclass for all staff types:

classDiagram class StaffMember { #String name #String address #String phone +StaffMember(name,address,phone) +toString() String +pay() double } class Employee { #String socialSecNum #double payRate +Employee(name,addr,phone,ssn,rate) +toString() String +pay() double } class Volunteer { +Volunteer(name,address,phone) +pay() double } class Executive { -double bonus +Executive(name,addr,phone,ssn,rate) +awardBonus(execBonus double) void +pay() double } class Hourly { -int hoursWorked +Hourly(name,addr,phone,ssn,rate) +toString() String +pay() double +addHours(moreHours int) void } StaffMember <|-- Employee StaffMember <|-- Volunteer Employee <|-- Executive Employee <|-- Hourly
📌 Reading the Hierarchy
  • StaffMember — the root. Holds shared data and defines pay() returning 0.
  • Employee extends StaffMember — adds SSN and payRate. Overrides pay().
  • Volunteer extends StaffMember — overrides pay() returning 0 (no salary).
  • Executive extends Employee — adds bonus. Overrides pay() = salary + bonus.
  • Hourly extends Employee — adds hoursWorked. Overrides pay() = hours × rate.

Every class in this hierarchy is connected by IS-A:

Executive IS-A Employee Employee IS-A StaffMember Executive IS-A StaffMember Volunteer IS-A StaffMember Hourly IS-A StaffMember

3 The Java Classes

Here are all five classes. Notice how each one overrides pay() in its own way:

👤 StaffMember.java — The Superclass
class StaffMember { protected String name; protected String address; protected String phone; public StaffMember(String name, String address, String phone) { this.name = name; this.address = address; this.phone = phone; } public String toString() { return "Name: " + name + ", Address: " + address; } // Base implementation — returns 0. Subclasses override this. public double pay() { return 0.0; } }
💼 Employee.java — extends StaffMember
class Employee extends StaffMember { protected String socialSecNum; protected double payRate; public Employee(String name, String address, String phone, String ssn, double payRate) { super(name, address, phone); this.socialSecNum = ssn; this.payRate = payRate; } public String toString() { return super.toString(); // "Name: ..., Address: ..." } public double pay() { return payRate; // fixed monthly salary } }
🤝 Volunteer.java — extends StaffMember
class Volunteer extends StaffMember { public Volunteer(String name, String address, String phone) { super(name, address, phone); } public String toString() { return "Name: " + name; // no address — volunteers are informal } // Volunteers receive no payment public double pay() { return 0.0; } }
👑 Executive.java — extends Employee
class Executive extends Employee { private double bonus; public Executive(String name, String address, String phone, String ssn, double payRate) { super(name, address, phone, ssn, payRate); this.bonus = 0.0; } // Grant a one-time bonus to be included in the next payment public void awardBonus(double execBonus) { bonus = execBonus; } public double pay() { double payment = super.pay() + bonus; // salary + bonus bonus = 0.0; // reset after paying return payment; } }
🕐 Hourly.java — extends Employee
class Hourly extends Employee { private int hoursWorked; public Hourly(String name, String address, String phone, String ssn, double payRate) { super(name, address, phone, ssn, payRate); this.hoursWorked = 0; } // Add hours worked — can be called many times per pay period public void addHours(int moreHours) { hoursWorked += moreHours; } public double pay() { double payment = hoursWorked * payRate; // hours × rate hoursWorked = 0; // reset after paying return payment; } public String toString() { return "Name: " + name + ", Hours: " + hoursWorked; } }

4 Building the Polymorphic Array

We declare the array as StaffMember[] — the superclass type. This lets us store any subclass object inside it, because of the IS-A rule.

📋 StaffDemo.java — Creating the Array
// Declared as StaffMember[] — but holds any subclass! StaffMember[] staff = new StaffMember[5]; // ── Fill with different types (upcasting is automatic) ── staff[0] = new Executive ("Dr. Khalid", "KAU, Jeddah", "055-0001", "EX-001", 5000.0); staff[1] = new Employee ("Faisal", "Riyadh", "055-0002", "EM-001", 3000.0); staff[2] = new Hourly ("Layla", "Jeddah", "055-0003", "HR-001", 50.0); staff[3] = new Volunteer ("Maha", "Mecca", "055-0004"); staff[4] = new Hourly ("Ziad", "Dammam", "055-0005", "HR-002", 45.0); // ── Setup: award bonus and log hours BEFORE payroll ─── ((Executive) staff[0]).awardBonus(2000.0); // downcast needed! ((Hourly) staff[2]).addHours(80); // downcast needed! ((Hourly) staff[4]).addHours(60); // downcast needed!

📊 What Does the Array Look Like in Memory?

Slot Declared Type   Actual Object Stored Name
staff[0] StaffMember ref Executive object Dr. Khalid  salary: 5000 + bonus 2000
staff[1] StaffMember ref Employee object Faisal  salary: 3000
staff[2] StaffMember ref Hourly object Layla  80 hrs × 50 SAR/hr
staff[3] StaffMember ref Volunteer object Maha  no payment
staff[4] StaffMember ref Hourly object Ziad  60 hrs × 45 SAR/hr
📌 Upcasting Happens Automatically

When we write staff[0] = new Executive(...), Java automatically upcasts the Executive reference to a StaffMember reference. This is safe because Executive IS-A StaffMember. No cast keyword is needed — Java does it silently.

5 Polymorphic Payroll — One Loop, All Types

Now comes the power of polymorphism. One loop iterates over all staff members and calls pay() on each. Java's dynamic binding picks the correct version of pay() at runtime based on the actual object type — not the declared type.

▶︎ The Polymorphic Loop
// ONE loop handles Executive, Employee, Hourly, AND Volunteer! String line = "-".repeat(74); System.out.printf("%-16s| %-41s| %s%n", "Type", "Details", "Payment (SAR)"); System.out.println(line); for (StaffMember s : staff) { System.out.printf("%-16s| %-41s| %,13.2f%n%n", s.getClass().getSimpleName(), // "Executive", "Hourly", etc. s, // calls toString() — dynamic binding! s.pay()); // calls pay() — dynamic binding! } System.out.println("=".repeat(74));
Output — Monthly Payroll
Type | Details | Payment (SAR) -------------------------------------------------------------------------- Executive | Name: Dr. Khalid, Address: KAU, Jeddah | 7,000.00 Employee | Name: Faisal, Address: Riyadh | 3,000.00 Hourly | Name: Layla, Hours: 80 | 4,000.00 Volunteer | Name: Maha | 0.00 Hourly | Name: Ziad, Hours: 60 | 2,700.00 ==========================================================================

Dynamic Binding Which pay() Gets Called?

The JVM looks at the actual object (not the declared type) to decide which pay() to execute at runtime:

Variable Declared type Actual object pay() executed Result
staff[0] StaffMember Executive Executive.pay() SAR 7,000
staff[1] StaffMember Employee Employee.pay() SAR 3,000
staff[2] StaffMember Hourly Hourly.pay() SAR 4,000
staff[3] StaffMember Volunteer Volunteer.pay() SAR 0
staff[4] StaffMember Hourly Hourly.pay() SAR 2,700
🎉 The Power of Dynamic Binding

The compiler only knows each variable is a StaffMember. But at runtime, the JVM inspects the actual object and calls the right pay(). Five different implementations — one line of code: s.pay().

6 Casting — Accessing Specific Methods

What if we want to log more hours for our Hourly workers? The method addHours() exists only in Hourly — it is NOT in StaffMember. The polymorphic reference s (declared as StaffMember) cannot see it directly.

❌ This Does NOT Compile
for (StaffMember s : staff) {
  // ERROR! StaffMember has no addHours()
  s.addHours(40); // ← compile error!
}

The compiler checks the declared type (StaffMember) and finds no addHours() there. Refused!

✅ Use instanceof + Downcast
for (StaffMember s : staff) {
  if (s instanceof Hourly) {
    Hourly h = (Hourly) s;
    h.addHours(40); // ✓ works!
  }
}

First check with instanceof, then downcast to Hourly. Now the compiler sees the full Hourly interface.

⬇ Full Casting Example — Log New Hours Mid-Period
// Scenario: mid-month, log 40 more hours for every Hourly worker for (StaffMember s : staff) { if (s instanceof Hourly) { Hourly h = (Hourly) s; // downcast: StaffMember ref → Hourly ref h.addHours(40); // now we can call Hourly-specific method System.out.println("Logged 40 hours for: " + h); } } // Output: // Logged 40 hours for: Layla (055-0003) [HR-001] | Rate: 50.0 SAR/hr // Logged 40 hours for: Ziad (055-0005) [HR-002] | Rate: 45.0 SAR/hr
🤔 Rule: What Can You See Through a Polymorphic Reference?

A StaffMember reference can only see methods defined in StaffMember. To access subclass-specific methods (like addHours() or awardBonus()), you must first check with instanceof, then downcast the reference.

📌 We Already Used Casting — Setup Before Payroll!

Remember the setup lines at the top of our program?

((Executive) staff[0]).awardBonus(2000.0);  // downcast to reach awardBonus()
((Hourly)    staff[2]).addHours(80);     // downcast to reach addHours()
((Hourly)    staff[4]).addHours(60);     // downcast to reach addHours()

Both awardBonus() and addHours() are not in StaffMember, so the polymorphic array cannot call them directly. Casting is the only way.

7 Design Discussion — The Volunteer Problem

Now let's think critically about this design. Our polymorphic loop calls s.pay() for every staff member. This works — but is it a good design?

⚠︎ The Problem: Volunteer Has a pay() That Means Nothing

Look at Volunteer.pay() — it returns 0.0. This method exists only because StaffMember declares it. In the real world, a volunteer is simply not payable — they shouldn't have a pay() method at all. But we forced it onto them by putting pay() in the superclass!

❌ Current Design — pay() in Superclass
StaffMember { pay() → 0 }
  ↳ Employee { pay() → salary }
    ↳ Executive { pay() → salary+bonus }
    ↳ Hourly { pay() → hrs×rate }
  ↳ Volunteer { pay() → 0 } ← forced!

Volunteer is not payable in real life. Forcing pay() onto it is a design compromise.

✅ Better Design — Payable Interface (Coming Later)
StaffMember { name, address, phone }
  ↳ Employee implements Payable
    ↳ Executive (inherits Payable)
    ↳ Hourly (inherits Payable)
  ↳ Volunteer  ← no pay() at all! ✓

With an interface, only paid staff implement Payable. Volunteer is cleanly excluded.

🏋︎ Class Discussion

👤 Student asks:
Doctor — if Volunteer.pay() always returns zero, why is it even there? Can't we just remove it?
💡 The Doctor replies:
Excellent question! We had to put pay() in StaffMember so that the polymorphic loop can call s.pay() on every element. If pay() was not in StaffMember, the compiler would refuse — because s is declared as StaffMember.

So yes — we forced Volunteer to have a method it conceptually does not need. This is a real design problem. It is not good design. But at this level, we accept it because we have no better tool yet.
👤 Student asks:
Then what is the better solution, Doctor?
🌟 Coming Soon — Interfaces!
The correct fix is to create a Payable interface with a pay() method. Only Employee (and its subclasses Executive and Hourly) would implement Payable. Volunteer would not implement it — so it would have no pay() at all.

The payroll loop would then iterate over Payable[] (not StaffMember[]) and Volunteer would simply not appear in the payroll. We will study this in the Interfaces chapter — stay tuned!
📌 Key Takeaway

This example shows that polymorphism with inheritance has limits. When a method in the superclass does not make conceptual sense for every subclass, it is a design smell. Interfaces give us a cleaner way to say: "only these specific classes are Payable" — without forcing the method onto everyone else.

8 Degrees of Polymorphism — In Our Staff System

Now that we have seen the payroll system in action, let's ask a critical question: how polymorphic is our design really? Polymorphism is not always 100% — the degree depends on how many subclasses genuinely and naturally share each behavior.

100%
All staff types share this — fully natural
toString(), getName() — every StaffMember has a name and an identity. Executive, Employee, Hourly, and Volunteer all genuinely have these. Placing them in StaffMember is 100% natural. This is perfect polymorphism!
75%
Most staff are paid — but not all
pay() — Executive, Employee, and Hourly genuinely earn a payment. But Volunteer.pay() returns 0.0 — a forced, artificial implementation. We put pay() in StaffMember to make the polymorphic loop work, even though a volunteer conceptually has no salary at all. This is ~75% natural — a reasonable compromise, but still a design smell.
25%
Role-specific behaviors — do NOT put in StaffMember!
addHours() — only Hourly staff log hours. awardBonus() — only Executive receives a bonus. These must NOT go into StaffMember. What would an Executive's hourly log even mean? What bonus does a Volunteer receive? These require downcasting — exactly what Section 6 demonstrated.
⚖ The Design Rule — Very Important!
Do NOT add a method to the superclass just to achieve 100% polymorphism.

Only add methods to the superclass that are naturally and reasonably shared by all (or most) subclasses. Forcing addHours() into StaffMember just to avoid casting would be bad design — it would give every Executive, Employee, and Volunteer an hours counter that makes no sense for their role.

It is perfectly fine to apply polymorphism at 75% or 80%. That is still excellent design. What matters is that the shared methods are conceptually meaningful for every subclass that inherits them.

🏋︎ Class Discussion

👤 Student asks:
Doctor — so pay() is not truly 100% polymorphic in our system?
💡 The Doctor replies:
Exactly right. Look at the four concrete types in our system:

Executive — salary + bonus → pay() is real and meaningful ✓
Employee — monthly salary → pay() is real ✓
Hourly — hours × rate → pay() is real ✓
Volunteer — returns 0.0 → pay() is forced and artificial

So pay() is natural for 3 out of 4 staff types. We accepted that trade-off to keep the payroll loop simple and uniform.
👤 Student asks:
But Doctor — if we cannot reach 100%, does that mean our design is wrong?
💡 The Doctor replies:
Not wrong — just a compromise that we knowingly accept. In real software, you rarely achieve 100% for every method. The key question is always: is the compromise reasonable?

Here, treating a Volunteer's pay as zero is understandable in a payroll report. But the cleaner solution is the Payable interface we discussed in Section 7 — where only genuinely payable staff implement the interface, and Volunteer is simply excluded from the payroll loop entirely. That design achieves 100% conceptual purity. We will build it in the Interfaces chapter!
👤 Student asks:
So how do we know when to stop trying to push a method into the superclass?
💡 The Doctor replies:
Ask yourself this question: "Does every subclass that inherits this method have a meaningful, real-world version of it?"

toString() in StaffMember? Yes — every staff member has an identity. Put it there. ✓
pay() in StaffMember? Mostly yes, but Volunteer is an exception. Acceptable compromise. ⚠
addHours() in StaffMember? No — only Hourly staff have hours. Keep it out! ✗

This reasoning is how experienced engineers decide what belongs in the superclass.

Summary — What You Learned in This Example

🏢
Real Hierarchy
5 classes in a 3-level hierarchy: StaffMember → Employee → Executive/Hourly + Volunteer.
📋
Polymorphic Array
StaffMember[] holds Executive, Employee, Hourly, and Volunteer objects together.
▶︎
Dynamic Binding
s.pay() calls the right version at runtime — Executive.pay(), Hourly.pay(), etc.
Casting
addHours() and awardBonus() need instanceof + downcast — they are not in StaffMember.
⚠︎
Design Smell
Putting pay() in StaffMember forces Volunteer to have a method it shouldn't need.
🌟
Coming: Interfaces
A Payable interface solves this cleanly — only paid staff implement it. Volunteer stays out.
📈
Degrees of Polymorphism
toString() is 100% natural. pay() is ~75% (Volunteer is forced). addHours()/awardBonus() stay out of the superclass.
← Polymorphism Abstract Classes →
🎨 Code Block Theme