Skip to content

Object-Oriented Programming

Object-oriented programming (OOP) models a program as collaborating objects with state, behavior, identity, and explicit responsibilities. Java supports OOP, but not every Java class represents a useful domain object and not every problem is best expressed through inheritance.

Learning objectives

  • distinguish encapsulation from merely making fields private;
  • distinguish abstraction, inheritance, subtyping, and polymorphism;
  • explain dynamic dispatch and substitutability;
  • prefer composition when inheritance does not represent a stable subtype; and
  • design objects that preserve their invariants.

Objects and classes

A class defines a type and an implementation template. An object is a runtime instance. Java variables of class or interface type normally hold references; assignment copies a reference, not the referenced object.

final class BankAccount {
    private long balanceInCents;

    BankAccount(long openingBalanceInCents) {
        if (openingBalanceInCents < 0) {
            throw new IllegalArgumentException("negative opening balance");
        }
        balanceInCents = openingBalanceInCents;
    }

    void withdraw(long amountInCents) {
        if (amountInCents <= 0 || amountInCents > balanceInCents) {
            throw new IllegalArgumentException("invalid withdrawal");
        }
        balanceInCents -= amountInCents;
    }

    long balanceInCents() {
        return balanceInCents;
    }
}

The important encapsulation is not the private modifier by itself. Every public operation protects the invariant balanceInCents >= 0; callers cannot place the object into an invalid state through an unrestricted setter.

Abstraction

An abstraction exposes behavior relevant to a client while hiding decisions the client should not depend on. Interfaces can express a role:

interface DiscountPolicy {
    Money discountFor(Order order);
}

The interface does not promise how discounts are stored or calculated. A useful abstraction is defined by a behavioral contract, not only by method signatures.

Inheritance and subtyping

Class inheritance reuses or specializes implementation. Subtyping creates a substitutability relationship: code written for a supertype must continue to satisfy its expectations when given a subtype.

sealed interface Shape permits Circle, Rectangle {
    double area();
}

record Circle(double radius) implements Shape {
    Circle {
        if (!(radius >= 0.0)) throw new IllegalArgumentException("negative radius");
    }

    @Override public double area() {
        return Math.PI * radius * radius;
    }
}

record Rectangle(double width, double height) implements Shape {
    Rectangle {
        if (!(width >= 0.0) || !(height >= 0.0)) {
            throw new IllegalArgumentException("negative dimension");
        }
    }

    @Override public double area() {
        return width * height;
    }
}

Calling area() through a Shape reference uses dynamic dispatch to select the runtime implementation. Overloading is different: the compiler selects among same-named method signatures using compile-time types.

Composition over implementation inheritance

Composition delegates one responsibility to a collaborator:

final class PriceCalculator {
    private final DiscountPolicy discounts;

    PriceCalculator(DiscountPolicy discounts) {
        this.discounts = Objects.requireNonNull(discounts);
    }

    Money totalFor(Order order) {
        return order.subtotal().subtract(discounts.discountFor(order));
    }
}

Different policies can vary independently of PriceCalculator. Inheritance is appropriate when the subtype contract is genuine and stable; use composition when the goal is only to reuse or replace behavior.

Identity and value

Some objects represent an identity that persists while attributes change. Others represent values and are equal solely by their contents. Immutable value objects are good candidates for records. Entity equality needs a lifecycle-aware identity policy, particularly when persistence generates identifiers.

Common mistakes

  • an anemic object with public setters that cannot defend its invariants;
  • a deep hierarchy built only for code reuse;
  • a “god object” coordinating unrelated responsibilities;
  • exposing mutable internal collections;
  • using inheritance where a subtype strengthens preconditions or surprises clients;
  • assuming private state makes an object thread-safe.

Exercises

  1. Refactor a conditional payment calculation into composed policies.
  2. Explain why a square with independently mutable width and height is not safely substitutable for such a rectangle abstraction.
  3. Design a value object that defensively owns a collection.