Overview
Every company pays different kinds of employees in fundamentally different ways — a salaried employee earns the same amount every period, an hourly employee earns based on hours worked, and a commission-based employee earns a percentage of what they sell. What all three genuinely share is not a formula, only a promise: every one of them is "payable," meaning you can always ask it for a salary. Java's interface is built to express exactly that kind of promise, separately from any shared implementation.
By the end of this tutorial you will have a console payroll application built around a Payable interface, an abstract Employee class that implements it without fully satisfying it, and three concrete subclasses that each override calculateSalary() with their own formula. Every employee, regardless of type, is stored in a single fixed-size Employee[] array, and running payroll is one loop that calls calculateSalary() on each element — the array never needs to know or care which concrete class any given element actually is.
- A
Payableinterface declaring a singlecalculateSalary()method. - An abstract
Employeeclass implementingPayablewithout providing a body forcalculateSalary(). - A
SalariedEmployeesubclass paying a fixed monthly amount. - An
HourlyEmployeesubclass payinghoursWorked * hourlyRate, with overtime past 160 hours. - A
CommissionEmployeesubclass paying a base plus a percentage of sales. - A
Payrollclass managing a fixed-sizeEmployee[]array and printing a full payroll report polymorphically.
Prerequisites
- Interfaces — declaring a
interfacewith an abstract method andimplements-ing it in a class. - Abstraction — the difference between an
interface(a pure contract) and anabstract class(a partial implementation). - Inheritance and polymorphism —
extends,@Override, and calling an overridden method through a base reference. - Arrays — declaring a fixed-size
Employee[], storing elements by index, and iterating with aforloop. - Method basics — return types, parameters, and calling one method from another.
Project Structure
The program lives in a single file, EmployeePayrollSystem.java. Payable is a pure interface: it has no fields and no method bodies, only the signature double calculateSalary();, which is implicitly public abstract. Employee is declared abstract class Employee implements Payable — it satisfies the "is a class that can be paid" promise structurally by implementing Payable, but it deliberately does not provide a body for calculateSalary() itself, re-declaring it as abstract. Java allows this specific combination: an abstract class is allowed to leave an interface method unimplemented, as long as the class itself is marked abstract, pushing the actual obligation down to whichever concrete subclass extends it.
Payroll owns a fixed-size Employee[] array rather than a growable ArrayList<Employee> — the array size is decided once in the constructor, and addEmployee() fills the next open slot, which is a deliberate, simpler alternative chosen here specifically to practice array mechanics (tracking a count of filled slots, checking for a full array) that a List would otherwise hide from you.
Step 1: Define the Payable Interface
An interface method has no body and, as of any modern Java version, no visibility modifier is needed on it — every method declared in an interface is public abstract by default unless marked default or static. Payable says nothing about how a salary is computed, only that any class implementing it must be able to answer the question.
// A pure contract: any class that "implements Payable" is promising it can// compute a salary, without saying anything about how. No fields, no bodies.interface Payable { double calculateSalary(); // Implicitly public and abstract; every implementer must define this}Step 2: Define an Abstract Employee Base Class
Every employee — regardless of pay structure — has an id and a name, so those live in Employee as protected fields that subclasses can read directly. Employee implements Payable but re-declares calculateSalary() as abstract rather than providing a formula, which is legal specifically because Employee itself is marked abstract class — the compiler defers the "you must implement this" obligation to whichever concrete subclass extends Employee.
abstract class Employee implements Payable { protected int id; // protected, not private, so subclasses can read it directly protected String name; // Employee's display name
public Employee(int id, String name) { this.id = id; this.name = name; }
public int getId() { return id; }
public String getName() { return name; }
// No calculateSalary() body here: Employee satisfies "implements Payable" structurally, // but leaves the real obligation to SalariedEmployee, HourlyEmployee, and CommissionEmployee.
public void printPayslip() { System.out.printf("ID: %-4d | Name: %-18s | Type: %-12s | Salary: %.2f%n", id, name, getEmployeeType(), calculateSalary()); // Both calls dispatch to the real subclass at runtime }
public abstract String getEmployeeType(); // A second abstract method, so payslips can label each employee's type}printPayslip() is itself never overridden by any subclass — it does not need to be. It already produces correct, type-specific output for any employee by calling the abstract calculateSalary() and getEmployeeType() internally, the same "small abstract methods power one larger shared method" pattern used by Member.displayInfo() in the Library Management System project.
Step 3: Derive SalariedEmployee and HourlyEmployee
SalariedEmployee is the simplest possible implementation: calculateSalary() just returns a fixed field. HourlyEmployee is more interesting — it multiplies hoursWorked by hourlyRate for the first 160 hours, then applies a 1.5x overtime multiplier to anything beyond that, all inside its own calculateSalary() override, completely invisible to any code that only knows it is holding an Employee.
class SalariedEmployee extends Employee { private double monthlySalary; // Fixed amount paid every period, regardless of hours
public SalariedEmployee(int id, String name, double monthlySalary) { super(id, name); // Forwards id and name to Employee's constructor this.monthlySalary = monthlySalary; }
@Override public double calculateSalary() { return monthlySalary; // No formula needed: salaried pay doesn't depend on hours or sales }
@Override public String getEmployeeType() { return "Salaried"; }}
class HourlyEmployee extends Employee { private double hoursWorked; // Hours worked this pay period private double hourlyRate; // Base pay per hour
public HourlyEmployee(int id, String name, double hoursWorked, double hourlyRate) { super(id, name); this.hoursWorked = hoursWorked; this.hourlyRate = hourlyRate; }
@Override public double calculateSalary() { double regularHours = Math.min(hoursWorked, 160); // Cap regular-rate hours at 160 per period double overtimeHours = Math.max(0, hoursWorked - 160); // Anything past 160 counts as overtime return (regularHours * hourlyRate) + (overtimeHours * hourlyRate * 1.5); // Overtime pays 1.5x }
@Override public String getEmployeeType() { return "Hourly"; }}Click Run to see what this code prints.
Step 4: Derive a CommissionEmployee
A third subclass drives home that calculateSalary() can be computed however each type needs — CommissionEmployee combines a flat base pay with a percentage of sales, a formula that shares nothing structurally with either SalariedEmployee or HourlyEmployee, yet all three are equally valid Employee objects to any code that only calls the methods Employee and Payable promise.
class CommissionEmployee extends Employee { private double baseSalary; // Guaranteed pay regardless of sales performance private double salesAmount; // Total sales generated this period private double commissionRate; // Fraction of sales paid as commission, e.g. 0.05 for 5%
public CommissionEmployee(int id, String name, double baseSalary, double salesAmount, double commissionRate) { super(id, name); this.baseSalary = baseSalary; this.salesAmount = salesAmount; this.commissionRate = commissionRate; }
@Override public double calculateSalary() { return baseSalary + (salesAmount * commissionRate); // Base pay plus a cut of whatever they sold }
@Override public String getEmployeeType() { return "Commission"; }}Step 5: Build the Payroll Class Around an Employee Array
Payroll allocates a fixed-size Employee[] in its constructor and tracks how many slots are actually filled with a count field, since a Java array (unlike an ArrayList) has no built-in notion of "how many elements are really in use" versus "how large the array is." addEmployee() checks count against the array's .length before writing, since writing past a full array would throw an ArrayIndexOutOfBoundsException.
class Payroll { private Employee[] employees; // Fixed-size array; every element is an Employee reference, concrete type varies private int count = 0; // How many slots are actually filled, since employees.length never shrinks or grows
public Payroll(int capacity) { employees = new Employee[capacity]; // Allocates capacity slots, all initially null }
public boolean addEmployee(Employee e) { if (count >= employees.length) { // Guard against writing past the array's fixed size System.out.println("Payroll is full; cannot add more employees."); return false; } employees[count] = e; // Store at the next open slot count++; return true; }
public void runPayroll() { System.out.println("\n--- Payroll Report ---"); double total = 0; for (int i = 0; i < count; i++) { // Only iterate over filled slots, not the whole array employees[i].printPayslip(); // Polymorphic: dispatches to each employee's real calculateSalary() total += employees[i].calculateSalary(); } System.out.printf("Total payroll this period: %.2f%n", total); }
public Employee findById(int id) { for (int i = 0; i < count; i++) { if (employees[i].getId() == id) { return employees[i]; } } return null; }}Step 6: Build the Menu Loop
main() creates a Payroll with a fixed capacity and loops over a menu that lets the user add any of the three employee types or run the full payroll report. Adding an employee is the one place the user's choice directly decides which concrete class gets constructed, while runPayroll() afterward treats every element of the array uniformly through its Employee type.
import java.util.Scanner;
public class EmployeePayrollSystem { public static void main(String[] args) { Payroll payroll = new Payroll(50); // Fixed capacity of 50 employees for this run Scanner scanner = new Scanner(System.in); int nextId = 1; int choice;
do { System.out.println("\n===== EMPLOYEE PAYROLL SYSTEM ====="); System.out.println("1. Add Salaried Employee"); System.out.println("2. Add Hourly Employee"); System.out.println("3. Add Commission Employee"); System.out.println("4. Run Payroll Report"); System.out.println("5. Find Employee by ID"); System.out.println("6. Exit"); System.out.print("Enter your choice: "); choice = scanner.nextInt(); scanner.nextLine(); // Discard the leftover newline left behind by nextInt()
if (choice == 1) { System.out.print("Enter name: "); String name = scanner.nextLine(); System.out.print("Enter monthly salary: "); double monthlySalary = scanner.nextDouble(); scanner.nextLine(); payroll.addEmployee(new SalariedEmployee(nextId, name, monthlySalary)); nextId++; System.out.println("Salaried employee added."); } else if (choice == 2) { System.out.print("Enter name: "); String name = scanner.nextLine(); System.out.print("Enter hours worked: "); double hours = scanner.nextDouble(); System.out.print("Enter hourly rate: "); double rate = scanner.nextDouble(); scanner.nextLine(); payroll.addEmployee(new HourlyEmployee(nextId, name, hours, rate)); nextId++; System.out.println("Hourly employee added."); } else if (choice == 3) { System.out.print("Enter name: "); String name = scanner.nextLine(); System.out.print("Enter base salary: "); double base = scanner.nextDouble(); System.out.print("Enter sales amount: "); double sales = scanner.nextDouble(); System.out.print("Enter commission rate (e.g. 0.05 for 5%): "); double rate = scanner.nextDouble(); scanner.nextLine(); payroll.addEmployee(new CommissionEmployee(nextId, name, base, sales, rate)); nextId++; System.out.println("Commission employee added."); } else if (choice == 4) { payroll.runPayroll(); } else if (choice == 5) { System.out.print("Enter ID to find: "); int id = scanner.nextInt(); scanner.nextLine(); Employee found = payroll.findById(id); if (found == null) { System.out.println("No employee with that ID."); } else { found.printPayslip(); } } else if (choice == 6) { System.out.println("Goodbye!"); } else { System.out.println("Invalid choice, try again."); // Catches anything outside 1-6 } } while (choice != 6); // Keep looping until the user explicitly picks Exit
scanner.close(); }}Complete Code
Here is the full program with every type assembled in the correct order, ready to save as EmployeePayrollSystem.java and run with javac EmployeePayrollSystem.java && java EmployeePayrollSystem.
import java.util.Scanner;
interface Payable { double calculateSalary();}
abstract class Employee implements Payable { protected int id; protected String name;
public Employee(int id, String name) { this.id = id; this.name = name; }
public int getId() { return id; }
public String getName() { return name; }
public void printPayslip() { System.out.printf("ID: %-4d | Name: %-18s | Type: %-12s | Salary: %.2f%n", id, name, getEmployeeType(), calculateSalary()); }
public abstract String getEmployeeType();}
class SalariedEmployee extends Employee { private double monthlySalary;
public SalariedEmployee(int id, String name, double monthlySalary) { super(id, name); this.monthlySalary = monthlySalary; }
@Override public double calculateSalary() { return monthlySalary; }
@Override public String getEmployeeType() { return "Salaried"; }}
class HourlyEmployee extends Employee { private double hoursWorked; private double hourlyRate;
public HourlyEmployee(int id, String name, double hoursWorked, double hourlyRate) { super(id, name); this.hoursWorked = hoursWorked; this.hourlyRate = hourlyRate; }
@Override public double calculateSalary() { double regularHours = Math.min(hoursWorked, 160); double overtimeHours = Math.max(0, hoursWorked - 160); return (regularHours * hourlyRate) + (overtimeHours * hourlyRate * 1.5); }
@Override public String getEmployeeType() { return "Hourly"; }}
class CommissionEmployee extends Employee { private double baseSalary; private double salesAmount; private double commissionRate;
public CommissionEmployee(int id, String name, double baseSalary, double salesAmount, double commissionRate) { super(id, name); this.baseSalary = baseSalary; this.salesAmount = salesAmount; this.commissionRate = commissionRate; }
@Override public double calculateSalary() { return baseSalary + (salesAmount * commissionRate); }
@Override public String getEmployeeType() { return "Commission"; }}
class Payroll { private Employee[] employees; private int count = 0;
public Payroll(int capacity) { employees = new Employee[capacity]; }
public boolean addEmployee(Employee e) { if (count >= employees.length) { System.out.println("Payroll is full; cannot add more employees."); return false; } employees[count] = e; count++; return true; }
public void runPayroll() { System.out.println("\n--- Payroll Report ---"); double total = 0; for (int i = 0; i < count; i++) { employees[i].printPayslip(); total += employees[i].calculateSalary(); } System.out.printf("Total payroll this period: %.2f%n", total); }
public Employee findById(int id) { for (int i = 0; i < count; i++) { if (employees[i].getId() == id) { return employees[i]; } } return null; }}
public class EmployeePayrollSystem { public static void main(String[] args) { Payroll payroll = new Payroll(50); Scanner scanner = new Scanner(System.in); int nextId = 1; int choice;
do { System.out.println("\n===== EMPLOYEE PAYROLL SYSTEM ====="); System.out.println("1. Add Salaried Employee"); System.out.println("2. Add Hourly Employee"); System.out.println("3. Add Commission Employee"); System.out.println("4. Run Payroll Report"); System.out.println("5. Find Employee by ID"); System.out.println("6. Exit"); System.out.print("Enter your choice: "); choice = scanner.nextInt(); scanner.nextLine();
if (choice == 1) { System.out.print("Enter name: "); String name = scanner.nextLine(); System.out.print("Enter monthly salary: "); double monthlySalary = scanner.nextDouble(); scanner.nextLine(); payroll.addEmployee(new SalariedEmployee(nextId, name, monthlySalary)); nextId++; System.out.println("Salaried employee added."); } else if (choice == 2) { System.out.print("Enter name: "); String name = scanner.nextLine(); System.out.print("Enter hours worked: "); double hours = scanner.nextDouble(); System.out.print("Enter hourly rate: "); double rate = scanner.nextDouble(); scanner.nextLine(); payroll.addEmployee(new HourlyEmployee(nextId, name, hours, rate)); nextId++; System.out.println("Hourly employee added."); } else if (choice == 3) { System.out.print("Enter name: "); String name = scanner.nextLine(); System.out.print("Enter base salary: "); double base = scanner.nextDouble(); System.out.print("Enter sales amount: "); double sales = scanner.nextDouble(); System.out.print("Enter commission rate (e.g. 0.05 for 5%): "); double rate = scanner.nextDouble(); scanner.nextLine(); payroll.addEmployee(new CommissionEmployee(nextId, name, base, sales, rate)); nextId++; System.out.println("Commission employee added."); } else if (choice == 4) { payroll.runPayroll(); } else if (choice == 5) { System.out.print("Enter ID to find: "); int id = scanner.nextInt(); scanner.nextLine(); Employee found = payroll.findById(id); if (found == null) { System.out.println("No employee with that ID."); } else { found.printPayslip(); } } else if (choice == 6) { System.out.println("Goodbye!"); } else { System.out.println("Invalid choice, try again."); } } while (choice != 6);
scanner.close(); }}Sample Run
Click Run to see what this code prints.
Extend This Project
- Add a second interface,
Bonusable, with adouble calculateBonus()method, and implement it only on the employee types that should receive a year-end bonus. - Replace the fixed-size
Employee[]with aList<Employee>and compare the two approaches directly — note howaddEmployee()no longer needs a capacity check. - Add a
TaxCalculatorutility class with a static method that applies a progressive tax rate tocalculateSalary()'s result before printing. - Sort the payroll report by salary using
Arrays.sort(employees, 0, count, Comparator.comparingDouble(Employee::calculateSalary)). - Persist each payroll run to a CSV file with
java.io.FileWriter, one line per employee, so historical payroll reports can be reviewed later.
Summary
You built a payroll system where three genuinely different salary formulas coexist behind one shared Payable promise and one abstract Employee class, stored together in a single fixed-size Employee[] array and processed by one polymorphic loop in runPayroll(). The interface-plus-abstract-class combination you used here — a contract with no implementation at all, and a partial implementation that still leaves the core method abstract — is a pattern you will see constantly in larger Java codebases, anywhere a family of types needs to share some behavior while still guaranteeing every member can answer one specific question in its own way.