Object-Oriented Programming (OOP): Four Pillars and a Worked Dynamic-Binding Example

Build a precise OOP mental model, then follow two Employee objects through a Java program to see overloading, overriding and runtime dispatch in action.

KnowledgeGate Team

Exam prep & CS education

Updated 10 Aug 20266 min read

Memorising encapsulation, abstraction, inheritance and polymorphism does not help when a question asks which method actually executes. You need a class-to-object mental model and a reliable way to trace a call. One Java payroll example supplies both: two employee objects, two pay formulas, one loop, and an output you can predict before running it.

Object-oriented programming starts with state, behaviour and identity

A class is a type-level blueprint. An object is one runtime instance with its own identity, state and behaviour. Employee can define the contract, while E1 and E2 are distinct objects. They remain distinct even if their fields temporarily contain equal values.

In a procedural design, a record may hold data while free functions operate on it. In an object-oriented design, an object can keep valid state behind methods and take responsibility for the operations that belong to it. OOP is therefore a way to assign responsibility, not merely syntax for putting functions inside a class.

If you would rather type these classes than read them, the Java module in Coding for Placements starts at JDK setup and works up through the language.

The four pillars of OOP solve four different design problems

Pillar

Design problem solved

Employee example

Encapsulation

Protect state behind a controlled boundary

Keep id, annualSalary, hours and hourlyRate inside their classes

Abstraction

Show the caller the needed operation, not its internal formula

Expose monthlyPay() through Employee

Inheritance

Let subtypes reuse and fulfil a common contract

SalariedEmployee and HourlyEmployee inherit the Employee contract

Polymorphism

Let one interface invoke different implementations

One Employee[] calls two versions of monthlyPay()

Encapsulation and abstraction are often confused. Encapsulation controls access to state. Abstraction controls which idea the caller needs to see. A private field helps encapsulation, but it is not the whole of abstraction.

Subtype polymorphism means either concrete employee can be used wherever an Employee is expected. Inheritance is not the only kind of reuse, and languages do not all implement dispatch in exactly the same way.

Classes, objects and relationships: know what belongs to the type

A constructor establishes an object's initial state. Instance fields and methods belong to each object, while class-level or static members belong to the type. Access control limits what outside code can reach. A reference identifies an object that exists for some runtime lifetime.

In the example below, private final fields and the @Override annotation are Java syntax. The underlying ideas, protected state and a subtype implementation of a shared operation, are language-neutral.

Relationships must also match the model:

  • HourlyEmployee is an Employee, so it can satisfy the employee contract.

  • Payroll has a List<Employee>, so it should contain or refer to employees.

  • Payroll extends Employee would be wrong because payroll cannot behave as an employee.

Association, aggregation and composition describe different ownership strengths. A payroll service can refer to employees without owning their lifetime. A payslip line can be modelled as a part whose lifetime belongs to its payslip. This is a modelling distinction, not one universal memory-management rule for Java, C++ and Python.

OOP worked example: trace two employees through one abstract interface

Employee supplies one stable interface, while each concrete subtype owns its pay formula. Save all four classes in Payroll.java and run Payroll.

abstract class Employee {
    private final String id;

    Employee(String id) {
        this.id = id;
    }

    String id() {
        return id;
    }

    abstract int monthlyPay();
}

class SalariedEmployee extends Employee {
    private final int annualSalary;

    SalariedEmployee(String id, int annualSalary) {
        super(id);
        this.annualSalary = annualSalary;
    }

    @Override
    int monthlyPay() {
        return annualSalary / 12;
    }
}

class HourlyEmployee extends Employee {
    private final int hours;
    private final int hourlyRate;

    HourlyEmployee(String id, int hours, int hourlyRate) {
        super(id);
        this.hours = hours;
        this.hourlyRate = hourlyRate;
    }

    @Override
    int monthlyPay() {
        return hours * hourlyRate;
    }
}

public class Payroll {
    public static void main(String[] args) {
        Employee[] team = {
            new SalariedEmployee("E1", 60000),
            new HourlyEmployee("E2", 160, 30)
        };

        int total = 0;
        for (Employee employee : team) {
            int pay = employee.monthlyPay();
            total += pay;
            System.out.println(employee.id() + ": " + pay);
        }
        System.out.println("Total: " + total);
    }
}

Trace it step by step:

  1. E1 is a SalariedEmployee, so its override computes 60000 / 12 = 5000 pay units.

  2. E2 is an HourlyEmployee, so its override computes 160 * 30 = 4800 pay units.

  3. The loop adds 5000 + 4800 = 9800 pay units.

The output is exactly:

E1: 5000
E2: 4800
Total: 9800

Private fields provide encapsulation. Abstract monthlyPay() provides abstraction. Both subclasses inherit the contract. The Employee[] loop exercises runtime polymorphism.

UML diagram of an abstract Employee class inherited by SalariedEmployee and HourlyEmployee, feeding a Payroll list that totals 9800.

Overloading, overriding, static binding and dynamic binding are not synonyms

Overloading gives methods the same name but different parameter lists:

void show(int value) { }
void show(String value) { }

show(42);   // selects show(int)
show("42"); // selects show(String)

The argument's compile-time type determines the selected signature. This is normally discussed as static binding.

Overriding replaces an inherited instance-method implementation in a subtype:

Employee e = new HourlyEmployee("E2", 160, 30);
int pay = e.monthlyPay();

The reference type exposes the Employee contract, but Java dispatches the overridden call to HourlyEmployee.monthlyPay(). It returns 160 * 30 = 4800.

The rule to carry into an exam is short: overload selection is static, overridden instance-method selection is dynamic. The specifics live at the boundary. Static, private and final methods, default arguments and multiple inheritance follow language-specific rules, so identify the language before applying any edge-case rule.

Dispatch trace of Employee references E1 and E2 routing monthlyPay() to 5000 and 4800, which sum to a total of 9800.

OOP traps: correct the model before tracing the code

Trap

Correction in the employee model

Abstraction means private fields

Private fields encapsulate state; monthlyPay() abstracts the pay operation

Inheritance represents has-a

HourlyEmployee is an Employee, but Payroll has employees

An overloaded signature is an overridden implementation

Overloading changes parameters; overriding supplies a subtype implementation

The reference type decides the overridden target

Runtime type HourlyEmployee selects its monthlyPay() override

Equal fields mean the same object

Equal state does not erase the separate identities of E1 and E2

Java interfaces and garbage collection, C++ multiple inheritance and deterministic destructors, and Python duck typing change implementation details. The four design questions still transfer, but syntax-level answers must follow the language in the question.

How OOP questions test understanding, not definitions

Common questions ask you to classify a relationship, identify a valid declaration, separate overload from override, trace constructor or method calls, or predict output through a base-type reference. Interviews often add a design follow-up, such as when composition is safer than inheritance.

Use the worked model as a self-check: an Employee[] holds E1 and E2. Which implementations execute, and what total prints? A complete answer names SalariedEmployee.monthlyPay(), HourlyEmployee.monthlyPay() and 9800, not only the final number.

For the wider tour, including abstract classes versus interfaces, read Object Oriented Technology Explained: Four Pillars in Java. To decide how many hours OOP deserves beside the heavier subjects, use GATE CS Subject Weightage: Where Hours Pay Off.

Object-oriented programming: the short version and next step

Keep this five-point retrieval checklist:

  1. An object combines identity, state and behaviour.

  2. Encapsulation protects state behind a controlled boundary.

  3. Abstraction exposes a stable contract.

  4. Inheritance should model a genuine is-a relationship.

  5. Dynamic dispatch selects an overridden implementation using runtime type.

Now add CommissionEmployee("E3", base = 2000, sales = 40000, rate = 5%). Its method should compute 0.05 * 40000 = 2000, then 2000 + 2000 = 4000 pay units. Without changing the payroll loop, the three-object total becomes 9800 + 4000 = 13800.

For runnable examples and question practice, continue with Java Course: Concepts, MCQs and Coding Questions. For a broader preparation route, use GATE Guidance by Sanchit Sir. The no-course alternative is equally concrete: implement the third subclass and explain every dispatch aloud.