An Employee Type Hierarchy in Java
This is a worked example of the core object-oriented tools in Java: inheritance, overriding, type hierarchies, type substitution, polymorphism, and interfaces. We will use a company's Employee class as the running example. It needs to represent several kinds of employees, each paid differently, but we still want to treat them all as one uniform type.
Different Kinds of Employees
Suppose we are writing payroll software for a company. Every employee has a name and an id, and the question is how the company pays each one.
We start with the simplest version of Employee:
public class Employee {
public String name;
public int id;
public double monthlyPay() {
// ...
}
}
Aside: The Employee class and its fields are public. I am keeping the fields open like this to keep the example simple. This goes against the principle of encapsulation and information hiding: in real code you would make these fields private and provide accessor methods.
We want to compute how much each employee is paid this month. Not everyone is paid the same way. Some employees are salaried: they earn a fixed annual salary, and each month they get one twelfth of it. Others are paid hourly: they earn a rate for every hour they work.
The first instinct is to add a field that records what kind of employee we have, and then branch on it:
enum EmploymentType { SALARIED, HOURLY }
public class Employee {
public String name;
public int id;
public EmploymentType type;
public double annualSalary; // used when SALARIED
public double hourlyRate; // used when HOURLY
public int hoursWorked; // used when HOURLY
public double monthlyPay() {
if (type == EmploymentType.SALARIED) {
return annualSalary / 12;
} else {
return hourlyRate * hoursWorked;
}
}
}
This works. But, to support both kinds of employee, the Employee class now carries annualSalary, hourlyRate, and hoursWorked all at once. For a salaried employee, hourlyRate and hoursWorked are never used. For an hourly employee, annualSalary is never used.
So every Employee object carries fields that do not apply to it. This leads to confusion and bugs, because the compiler cannot stop us from creating invalid states.
Now suppose the company hires contractors, who are paid a fixed fee per project. We add a third kind of employee:
enum EmploymentType { SALARIED, HOURLY, CONTRACTOR }
We add yet another field, projectFee, and we extend the branch in monthlyPay:
public double monthlyPay() {
if (type == EmploymentType.SALARIED) {
return annualSalary / 12;
} else if (type == EmploymentType.HOURLY) {
return hourlyRate * hoursWorked;
} else {
return projectFee;
}
}
But monthlyPay is not the only method that depends on the kind of employee. Suppose we also want a yearly bonus, paid only to salaried employees:
public double bonus() {
if (type == EmploymentType.SALARIED) {
return annualSalary * 0.10;
} else if (type == EmploymentType.HOURLY) {
return 0;
} else {
return 0;
}
}
The same if/else over type appears again. And it appears in every method that has to compute a different result for each kind of employee. Each new kind of employee means another field that does not apply to everyone, another branch in monthlyPay, and another branch in bonus.
The problem is that we are trying to describe several different kinds of employee with one class. Instead, we want a separate class for each kind of employee, each carrying only the fields it needs, and each with its own version of monthlyPay. But we still want to treat all of them as Employee objects, so the rest of our code can work with them in a uniform way.
Inheritance in Java
The idea is to start from a general class that holds what every employee has in common, and then define more specialized classes on top of it. Java lets us do this through inheritance.
We keep the shared part in Employee:
public class Employee {
public String name;
public int id;
public Employee(String name, int id) {
this.name = name;
this.id = id;
}
}
Notice what is no longer here: there is no type field, no annualSalary, no hourlyRate. Employee now holds only what is true of every employee: a name and an id.
Now we describe a salaried employee as a specialized Employee:
public class SalariedEmployee extends Employee {
public double annualSalary;
public SalariedEmployee(String name, int id, double annualSalary) {
super(name, id);
this.annualSalary = annualSalary;
}
public double monthlyPay() {
return annualSalary / 12;
}
}
The keyword extends is what creates the relationship. It says that SalariedEmployee is an Employee, and that it should inherit everything Employee has. So even though we never wrote name or id inside SalariedEmployee, every SalariedEmployee has them:
SalariedEmployee s = new SalariedEmployee("Ada", 1, 90000);
System.out.println(s.name); // "Ada" — inherited from Employee
System.out.println(s.monthlyPay()); // 7500.0 — defined in SalariedEmployee
We call Employee the superclass and SalariedEmployee a subclass.
The line super(name, id) in the constructor of SalariedEmployee calls the Employee constructor to set up the inherited fields. Only then does the subclass set up its own. This is the reuse we wanted: SalariedEmployee does not repeat the code for name and id; it inherits it from Employee.
The other kinds of employee follow the same pattern. Each one adds only the fields it actually needs:
public class HourlyEmployee extends Employee {
public double hourlyRate;
public int hoursWorked;
public HourlyEmployee(String name, int id, double hourlyRate, int hoursWorked) {
super(name, id);
this.hourlyRate = hourlyRate;
this.hoursWorked = hoursWorked;
}
public double monthlyPay() {
return hourlyRate * hoursWorked;
}
}
public class ContractorEmployee extends Employee {
public double projectFee;
public ContractorEmployee(String name, int id, double projectFee) {
super(name, id);
this.projectFee = projectFee;
}
public double monthlyPay() {
return projectFee;
}
}
The problems from before are solved:
- There are no fields that do not apply. A
SalariedEmployeehas anannualSalary; it has nohourlyRateto set by mistake. - There is no
typefield and noif/elseover it. Each kind of employee computes its own pay in its ownmonthlyPay. - Adding a new kind of employee no longer means editing existing classes. We just write one more subclass.
Overriding in Java
Each subclass has its own monthlyPay, but Employee itself does not declare one. Code that only sees an Employee cannot call monthlyPay on it. We want every Employee to declare a monthlyPay, with each subclass supplying its own version.
So let's add a monthlyPay to Employee:
public class Employee {
public String name;
public int id;
// ...
public double monthlyPay() {
return ???;
}
}
But what should it return? Employee alone does not know whether this person is salaried, hourly, or a contractor. There is no sensible number to put here. An employee in general does not have a pay. Only specific kinds of employee do.
Java has a way to say this. We can declare the method without a body and mark it abstract:
public abstract class Employee {
public String name;
public int id;
public Employee(String name, int id) {
this.name = name;
this.id = id;
}
public abstract double monthlyPay();
}
An abstract method is a declaration without an implementation. It means every Employee has a monthlyPay, but Employee itself does not specify how to compute it.
Because the class now has a method with no body, the class itself must be marked abstract too (as in public abstract class Employee).
A subclass supplies the method body. This is called overriding:
public class SalariedEmployee extends Employee {
public double annualSalary;
public SalariedEmployee(String name, int id, double annualSalary) {
super(name, id);
this.annualSalary = annualSalary;
}
@Override
public double monthlyPay() {
return annualSalary / 12;
}
}
The @Override annotation tells Java, and the reader, that this method is meant to override one from the superclass. It is optional, but you should always use it. If you misspell the method name or declare different parameters, the compiler reports an error instead of silently creating an unrelated method.
The other subclasses override monthlyPay in the same way, each with its own computation:
public class HourlyEmployee extends Employee {
// ... fields and constructor as before ...
@Override
public double monthlyPay() {
return hourlyRate * hoursWorked;
}
}
public class ContractorEmployee extends Employee {
// ... fields and constructor as before ...
@Override
public double monthlyPay() {
return projectFee;
}
}
Overriding is not only for abstract methods. A subclass can also override a concrete method to replace behavior it would otherwise inherit. For example, every class inherits equals and hashCode from Object. A Student class can override both with versions that make sense for students.
Two consequences follow.
First, because monthlyPay is abstract in Employee, every concrete subclass must provide or inherit an implementation of monthlyPay. If, for example, HourlyEmployee forgot to override it, the code would not compile.
Second, we can no longer create a plain Employee. Writing new Employee("Ada", 1) is now a compile-time error because there is no concrete implementation of the monthlyPay method. A class marked abstract cannot be instantiated. You can only create objects of a subclass that implements all of its abstract methods.
Type Hierarchy in Java
We now have a small group of classes: a general Employee at the top, and three specialized kinds beneath it. This arrangement of classes is called a type hierarchy.
┌───────────────────┐
│ Employee │
└───────────────────┘
△ △ △
│ │ │
┌───────────┘ │ └───────────┐
│ │ │
┌──────────────────┐ ┌────────────────┐ ┌────────────────────┐
│ SalariedEmployee │ │ HourlyEmployee │ │ ContractorEmployee │
└──────────────────┘ └────────────────┘ └────────────────────┘
In this diagram, the boxes represent classes, and the arrows represent inheritance relationships. The arrows point upward from each subclass to its superclass. The direction matters, because it records an is-a relationship. The relationship does not hold in the other direction. Every SalariedEmployee is an Employee, but not every Employee is a SalariedEmployee.
In the same way that int and String are types in Java, every class we write is a new type. So the diagram above shows four types:
Employeeis the base type. It sits at the top and holds what every employee has in common.SalariedEmployee,HourlyEmployee, andContractorEmployeeare subtypes. Each sits belowEmployeeand adds what makes it specific.
The subtype is always the more specific type, and the base type is always the more general one.
Type Substitution in Java
The is-a relationship has a consequence: if a SalariedEmployee really is an Employee, then anywhere our code expects an Employee, we should be able to pass it a SalariedEmployee instead. Using a value of a subtype where a value of the base type is expected is called type substitution.
The simplest form is an assignment. We can declare a variable of type Employee and store a SalariedEmployee in it:
Employee e = new SalariedEmployee("Ada", 1, 90000);
This is allowed because every SalariedEmployee is an Employee. The variable e has type Employee, and the object it points to really is a SalariedEmployee, a specific kind of Employee.
Type substitution is what lets us treat a mixed group of employees the same way. A company does not keep a separate list for its salaried staff, its hourly staff, and its contractors. It keeps one list of all its employees declared as Employee[]:
Employee[] staff = new Employee[3];
staff[0] = new SalariedEmployee("Ada", 1, 90000);
staff[1] = new HourlyEmployee("Lin", 2, 30, 160);
staff[2] = new ContractorEmployee("Sam", 3, 5000);
Each slot holds a different kind of employee, but every slot has type Employee, so they fit in one array.
The same thing works for method parameters. A method that accepts an Employee can be called with any subtype:
public static void printBadge(Employee e) {
System.out.println(e.name + " (#" + e.id + ")");
}
printBadge(staff[0]); // a SalariedEmployee
printBadge(new ContractorEmployee("Sam", 3, 5000)); // a ContractorEmployee
We write printBadge once, against the general type Employee. It works for every kind of employee we have now, and it will work for any kind we add later.
What you can access through the base type
When a variable has type Employee, the compiler only knows that it is some Employee. So through that variable you can use the fields and methods that every Employee has but not the fields and methods that only its subtypes have:
Employee e = new SalariedEmployee("Ada", 1, 90000);
System.out.println(e.name); // ok — every Employee has a name
System.out.println(e.monthlyPay()); // ok — Employee promises a monthlyPay
System.out.println(e.annualSalary); // error — Employee has no annualSalary field
The object really does have an annualSalary, but the variable's type is Employee, and Employee declares no annualSalary field. The compiler uses the declared type of the variable, not the type of the object it currently holds.
Employee e = new SalariedEmployee("Ada", 1, 90000);
// ^^^^^^^^ ^^^^^^^^^^^^^^^^
// declared type actual type
The declared type (also called the apparent type) is the type written in the declaration: Employee. It is fixed, and the compiler uses it to decide what you are allowed to do with e. The actual type (also called the runtime type) is the class of the object actually created: SalariedEmployee.
The declared type decides which methods you can call on e. The actual type decides which version of the method actually runs.
Polymorphism in Java
We can run payroll over the whole company with a single loop:
Employee[] staff = new Employee[3];
staff[0] = new SalariedEmployee("Ada", 1, 90000);
staff[1] = new HourlyEmployee("Lin", 2, 30, 160);
staff[2] = new ContractorEmployee("Sam", 3, 5000);
double total = 0;
for (Employee e : staff) {
total += e.monthlyPay();
}
Look at the call e.monthlyPay(). It is the same expression at every iteration, and e has the same declared type, Employee. But the object e refers to has a different actual type each time: first a SalariedEmployee, then an HourlyEmployee, then a ContractorEmployee. The program picks which implementation of monthlyPay to run based on the actual type of the object. This is called dynamic dispatch.
We call it polymorphism when the same call expression can take different forms depending on the actual type of the object. The word comes from the Greek for "many forms."
Managers: is-a and has-a
Consider the Manager class below. It extends SalariedEmployee, so it is a kind of employee. It also has a reports field, which is a list of employees that report to this manager.
public class Manager extends SalariedEmployee {
public List<Employee> reports;
public Manager(String name, int id, double annualSalary) {
super(name, id, annualSalary);
this.reports = new ArrayList<>();
}
public void addReport(Employee e) {
reports.add(e);
}
}
Notice Manager does not override monthlyPay. It inherits the one from SalariedEmployee, which is exactly what we want: a manager is paid a salary, just like any other salaried employee.
The reports field is a has-a relationship, built by holding another object. A Manager has a list of employees. Building one object out of others like this is called composition.
Because a Manager is an Employee, type substitution applies: a Manager can go anywhere an Employee is expected, including the payroll roster and loop from earlier.
Employee[] staff = new Employee[2];
staff[0] = new Manager("Ada", 1, 200000);
staff[1] = new HourlyEmployee("Lin", 2, 30, 160);
double total = 0;
for (Employee e : staff) {
total += e.monthlyPay();
}
The loop does not change at all. For the manager, dynamic dispatch finds the monthlyPay it inherited from SalariedEmployee; for the hourly employee, it finds the monthlyPay defined in HourlyEmployee.
Interfaces in Java
Inheritance works well when classes share a base type, but sometimes they do not. A company pays its employees. It also pays its vendors, which are outside companies it buys services from. A Vendor is not an Employee. It is not a person on staff, and it has no salary. An Employee is not a Vendor either. The two classes share no useful base class in our design, and they share no fields. But the company has to pay both. The one thing they have in common is that each one can report the amount it is owed.
That shared capability is a contract, and Java lets us write a contract as an interface:
public interface Payable {
double amountDue();
}
For our purposes, an interface is a list of method declarations. It has no method bodies and no instance fields. Each method in it is like an abstract method: a declaration without an implementation. In current versions of Java, interfaces can also contain constants and default or static methods, but I like to think of interfaces as pure contracts.
A class declares that it fulfills the contract with the keyword implements. By writing implements Payable, the Vendor class takes on the responsibility of providing an implementation for every method the interface lists. If it does not, the compiler reports an error.
public class Vendor implements Payable {
public String company;
public double invoiceTotal;
public Vendor(String company, double invoiceTotal) {
this.company = company;
this.invoiceTotal = invoiceTotal;
}
@Override
public double amountDue() {
return invoiceTotal;
}
}
The Employee hierarchy can fulfill the same contract:
public abstract class Employee implements Payable {
// name, id, and constructor as before
public abstract double monthlyPay();
@Override
public double amountDue() {
return monthlyPay();
}
}
The Employee class says that the amount due to an employee is that employee's monthly pay. Every subclass of Employee inherits this implementation, so it does not have to override it. A SalariedEmployee, an HourlyEmployee, and a ContractorEmployee are all Payable, and they all have an amountDue method.
We can now use type substitution to write one method that pays any Payable, without knowing which class the object is an instance of:
public static void cutCheck(Payable p) {
System.out.println("Pay $" + p.amountDue());
}
cutCheck(new Vendor("Acme Cloud", 4000)); // a Vendor
cutCheck(new SalariedEmployee("Ada", 1, 90000)); // an Employee
A Vendor and a SalariedEmployee have no useful domain-specific superclass in common, but both of them are Payable, so both can be passed to cutCheck. The same rules that govern base types apply to interface types too. A variable Payable p can hold an object of any class that implements Payable, which is type substitution, and p.amountDue() runs that object's own version, which is dynamic dispatch.
Inheritance gives us reuse. It pulls shared code up into a base class. The kind of interface we are using here has no reusable code and no instance fields. What it gives us is capability. It names something a class can do, and it lets unrelated classes declare that they can do it. A class extends only one base class, but it can implement any number of interfaces.