# Object-oriented programming

Source: https://codewiki.com/java/oop/

> - **what**: Java OOP uses classes to define an object's state, behavior, and boundary, then operates on objects through references. An object isn't a field container; it's a collaborating unit responsible for preserving its contract.
> - **trap**: `private` fields don't automatically provide encapsulation, and `extends` doesn't automatically provide substitutability. Exposed mutable data or a subtype that breaks its base contract still produces a fragile model.
> - **fix**: Establish invariants in constructors, change state through behavioral methods, and call through the smallest useful abstraction. Inherit only when the subtype preserves the base contract; otherwise, use composition.

## What it is and why it exists

Object-oriented programming (OOP) puts related state and the behavior that operates on it into objects. A class declares which fields an object may store, how it is constructed, and which methods callers can invoke; an object is a runtime instance with its own identity and state. Java also has primitive types and static members, so calling it a “pure object-oriented language” is inaccurate.

An object boundary answers who is responsible for state. If any caller can assign an account balance, every call site must repeat the rules; if the account exposes only `deposit()` and `withdraw()`, validation stays inside the object that owns the balance. Keeping a representation behind an API this way is encapsulation.

A class invariant is a rule that must hold after construction and at the end of every public operation. An account balance may never be negative, an order may require at least one line, and a date range's end may not precede its start. Constructors and every state-changing method share responsibility for preserving these rules.

Inheritance declares a nominal “is-a” relationship, while runtime dispatch routes a base-type call to the actual object's overriding implementation. This creates subtype polymorphism: callers depend on a stable base contract, and different subtypes supply different implementations. Polymorphism is valuable because it removes concrete-class branches from callers, not merely because it reduces repeated code.

When an object holds collaborators in fields and delegates some work to them, it uses composition. Composition can replace a strategy at construction time without exposing implementation details as a subclass contract. You'll meet encapsulation, polymorphism, and composition together in domain models, services, strategies, adapters, and dependency-injection boundaries.

## How it works

A class is a declaration; a `new` expression creates an object and returns a reference value. A reference variable doesn't contain the object itself. Assigning it to another variable copies only the reference, so both variables can identify the same mutable object. `null` means no object, and invoking an instance method or accessing an instance field through it throws `NullPointerException`.

Each construction first gives instance fields default values, then executes the constructor chain and field-initialization logic. Constructors have no return type and aren't inherited; their job is to put a new object into a valid state before it escapes to callers. Pass required data as constructor arguments and reject invalid combinations immediately instead of creating a partial object for later setters to repair.

Access control determines which source locations may name a member. `private` limits access to the top-level class and its nested members, package access limits it to the same package, `protected` also opens access to qualifying subclass code, and `public` opens it wherever the class itself is accessible. Access modifiers are only a syntax boundary; real encapsulation also requires public methods not to leak mutable internal representation.

An instance-method call involves both static selection and runtime selection. The compiler uses the reference's static type, method name, and arguments to choose a signature. If the target is an overridable instance method, the JVM then uses the actual object's class to choose the final overriding implementation. `static` methods are resolved from declarations visible at the call site, `private` methods can't be overridden by subclasses, and `final` methods explicitly prohibit overriding.

Method overloading and method overriding are different mechanisms. Overloading provides different parameter lists under one name, and compile-time argument types select the target. Overriding provides a compatible instance method in a subtype, and the runtime object selects the target. Put `@Override` on overriding methods so the compiler can catch spelling errors, parameter changes, and accidental overloads.

### Behavioral contracts at call boundaries

A method signature states a name, parameter types, return type, and declared checked exceptions, but it can't describe all behavior. Callers also need to know which inputs are valid, whether `null` is accepted, what units a result uses, and whether failure changes state. These promises determine whether two implementations can substitute for each other.

Exceptions are observable results of an object API. If one implementation returns `false` for insufficient funds while another makes the balance negative or throws an unrelated exception, their Java signatures match but their behavioral contracts don't. Failure-path tests should assert both the exception or return value and whether the object retained its prior state.

Side effects need explicit boundaries. If a method that appears to calculate a price also writes a database, sends email, or updates a cache, callers can't safely retry or reorder calls. Separating commands from queries doesn't require a particular architecture, but names and documentation should make state changes predictable.

Concurrency safety likewise doesn't follow from `private` or `final`. When threads share a mutable object, reads, checks, and writes may interleave and break an invariant that holds in single-threaded code. An object should declare whether it is immutable, thread-confined, or protects shared state through synchronization.

A substitutable object contract answers at least these questions:

- Which argument values and object states permit the call?
- What are the normal result, units, and rounding rules?
- Does failure use a return value or exception, and what state remains afterward?
- Which objects may the method mutate, which I/O may it perform, and what threading model does it require?

Interfaces, abstract classes, and ordinary base classes can express only part of these answers. Documentation states the semantics, constructors enforce initial conditions, method implementations protect state, and contract tests subject different implementations to the same observable assertions.

### Abstraction, `final`, and extension boundaries

An abstract class can hold instance state, define construction rules, supply concrete methods, and leave selected operations abstract. It fits related types that genuinely share a lifecycle and invariants. If callers need only a behavior contract and implementations shouldn't share state, prefer considering an interface.

A `final` class prohibits subtypes, a `final` method prohibits overrides, and a `final` field prohibits reassignment. They protect different boundaries. In particular, a `final List` may still refer to a list with mutable contents, so `final` isn't a synonym for deep immutability.

`super(...)` selects a direct-base constructor. Java 25 permits a restricted constructor prologue before that invocation, but the early-construction context can't use the object under construction; older source levels require the invocation first. `super.method()` can call the base implementation replaced by the current override, but doing so couples the subtype to a base implementation sequence. A subtype should depend on that sequence only when base documentation makes it part of the extension contract.

A `protected` field lets subtypes bypass base behavior and change representation directly, widening the code responsible for preserving invariants. Usually, keep fields private and expose narrow protected operations or queries. The base can then change representation without forcing every subtype to change with it.

Every open override point is an API for future unknown code. If you can't state what a subtype may assume and what it must preserve, keep the method or class `final` until a real extension requirement establishes that boundary.

Review an object model in this order:

1. Write the invariants that must hold when construction finishes.
2. Name the object that owns each piece of mutable state and the methods allowed to change it.
3. Define the smallest observable contract from the caller's perspective, including results, exceptions, and side effects.
4. Decide whether subtype polymorphism or object composition should carry each variation point.
5. Verify the contract with invalid construction, boundary inputs, and substitute implementations.

| Design mechanism | Binding time | Best for | Main risk |
| --- | --- | --- | --- |
| Overloading | Compile time | Different parameter shapes for one operation | Assuming runtime argument types choose the method |
| Overriding | Runtime | Different subtype implementations of one contract | A subtype changing preconditions or result meaning |
| Composition | Construction or configuration | Replaceable collaborators and strategies | Unclear ownership and mutability boundaries |

## Examples

These three programs move from state encapsulation to runtime dispatch and object composition. Their output was produced locally with OpenJDK 21.0.12 and `javac --release 21 -Xlint:all`; they use only language and library features that remain valid in the target Java 25.

### Protecting a balance invariant with behavior

`BankAccount` neither exposes its balance field nor provides a setter that can assign any balance. The constructor rejects an invalid initial state, and both business methods check their preconditions before changing the balance.

<!-- quick -->

```java
// file: BankAccountDemo.java
public class BankAccountDemo {
    static final class BankAccount {
        private final String number;
        private long balanceInCents;

        BankAccount(String number, long openingBalance) {
            if (number == null || number.isBlank()) {
                throw new IllegalArgumentException("number is required");
            }
            if (openingBalance < 0) {
                throw new IllegalArgumentException("negative opening balance");
            }
            this.number = number;
            this.balanceInCents = openingBalance;
        }

        void deposit(long amount) {
            requirePositive(amount);
            balanceInCents += amount;
        }

        boolean withdraw(long amount) {
            requirePositive(amount);
            if (amount > balanceInCents) {
                return false;
            }
            balanceInCents -= amount;
            return true;
        }

        String summary() {
            return number + " balance=" + balanceInCents;
        }

        private static void requirePositive(long amount) {
            if (amount <= 0) {
                throw new IllegalArgumentException("amount must be positive");
            }
        }
    }

    public static void main(String[] args) {
        var account = new BankAccount("A-17", 10_000);
        account.deposit(2_500);
        System.out.println(account.withdraw(4_000));
        System.out.println(account.withdraw(9_000));
        System.out.println(account.summary());
    }
}
```

```text
true
false
A-17 balance=8500
```


<!-- /quick -->

The first withdrawal succeeds and changes state; the second fails without changing state, so the balance remains 8,500 cents. The `boolean` result models insufficient funds as an expected business outcome, while an invalid amount throws. A production money model must also define currency, overflow policy, and concurrency semantics; none of those follow automatically from using `long`.

The value of `private` isn't hiding the number itself but ensuring every write path passes through one set of rules. If `getBalance()` returned a mutable ledger object, or another method skipped validation and assigned the field, syntactic privacy still wouldn't preserve the invariant.

### Triggering runtime dispatch through a base type

`ShippingMethod` centralizes validation and output formatting in `quoteFor()` while leaving cost calculation to subclasses. The loop knows only the base type, but each actual object runs its corresponding `costInCents()` implementation.

```java
// file: DispatchDemo.java
import java.util.List;

public class DispatchDemo {
    abstract static class ShippingMethod {
        private final String label;

        ShippingMethod(String label) {
            this.label = label;
        }

        final String quoteFor(int kilograms) {
            if (kilograms <= 0) {
                throw new IllegalArgumentException("weight must be positive");
            }
            return label + ": " + costInCents(kilograms);
        }

        protected abstract int costInCents(int kilograms);
    }

    static final class Pickup extends ShippingMethod {
        Pickup() {
            super("pickup");
        }

        @Override
        protected int costInCents(int kilograms) {
            return 0;
        }
    }

    static final class Courier extends ShippingMethod {
        Courier() {
            super("courier");
        }

        @Override
        protected int costInCents(int kilograms) {
            return 300 + kilograms * 80;
        }
    }

    public static void main(String[] args) {
        List<ShippingMethod> methods = List.of(new Pickup(), new Courier());
        for (ShippingMethod method : methods) {
            System.out.println(method.quoteFor(3));
        }
    }
}
```

```text
pickup: 0
courier: 540
```

`quoteFor()` is `final`, so every subtype retains the positive-weight check; only the protected cost calculation varies. This template-method design works only while every shipping method can honor the same input and output contract. A method that accepts zero weight or returns another unit isn't a safely substitutable subtype.

Declaring the list elements as `ShippingMethod` neither copies nor wraps the actual objects. The reference's static type limits the members directly visible to callers, while the actual object type selects an override. Adding a contract-preserving subtype requires no new `instanceof` branch in the loop.

### Replacing a calculation policy with composition

`Checkout` owns the total calculation but delegates tax calculation to `TaxPolicy`. The same class can compose different policies without creating one `Checkout` subclass per tax regime.

```java
// file: CompositionDemo.java
public class CompositionDemo {
    interface TaxPolicy {
        long taxFor(long subtotalInCents);
    }

    static final class FixedRateTax implements TaxPolicy {
        private final int basisPoints;

        FixedRateTax(int basisPoints) {
            if (basisPoints < 0) {
                throw new IllegalArgumentException("negative tax rate");
            }
            this.basisPoints = basisPoints;
        }

        @Override
        public long taxFor(long subtotalInCents) {
            return Math.multiplyExact(subtotalInCents, basisPoints) / 10_000;
        }
    }

    static final class Checkout {
        private final TaxPolicy taxPolicy;

        Checkout(TaxPolicy taxPolicy) {
            this.taxPolicy = java.util.Objects.requireNonNull(taxPolicy);
        }

        long totalFor(long subtotalInCents) {
            if (subtotalInCents < 0) {
                throw new IllegalArgumentException("negative subtotal");
            }
            return Math.addExact(subtotalInCents, taxPolicy.taxFor(subtotalInCents));
        }
    }

    public static void main(String[] args) {
        var standard = new Checkout(new FixedRateTax(2_000));
        var exempt = new Checkout(subtotal -> 0);

        System.out.println(standard.totalFor(12_500));
        System.out.println(exempt.totalFor(12_500));
    }
}
```

```text
15000
12500
```

The fixed-rate object preserves a nonnegative-rate invariant, while the checkout owns the nonnegative-subtotal and non-null-policy boundaries. `Math.multiplyExact()` and `Math.addExact()` make integer overflow fail explicitly instead of wrapping silently. Division still truncates, so the business contract must define monetary rounding.

Composition doesn't automatically produce a good design. `TaxPolicy` must still say whether negative tax is permitted, how it rounds, whether it may access external state, and whether concurrent calls are safe. Constructor injection only makes the dependency visible; it doesn't fill in a missing behavioral contract.

## Pitfalls

### Equating private fields with encapsulation

> **Pitfall:** Mechanically generating a getter and setter for every field reduces an object to a data bag that outside code may rewrite freely. Independent setter validation can also permit invalid cross-field combinations, such as an end time before a start time.

**Fix:** name public methods after domain operations, and have one operation validate and update related fields atomically. Expose only the queries callers need. When returning a collection or array, define ownership and make a defensive copy where required.

### Inheriting to reuse a few lines

> **Pitfall:** A subtype inherits visible API, lifecycle assumptions, and override points, not just a few implementation lines. If it must reject inputs the base accepts or change result units and side effects, it breaks the substitution contract.

**Fix:** write the preconditions, postconditions, and invariants on which base-type callers rely, then run the same contract tests against every subtype. Prefer composing a collaborator when you only need implementation reuse or a replaceable strategy.

### Calling overridable methods from constructors

> **Pitfall:** When a base constructor calls an overridable instance method, dynamic dispatch can enter the subtype before its fields finish initialization. Generated code often creates an “initialization hook” this way, then reads default values or leaks a partially constructed `this`.

**Fix:** during construction, call only controlled `private`, `static`, or `final` logic. To extend building, use a factory that returns only after validation, an explicit initialization phase, or a fully constructed collaborator supplied as data.

### Confusing overloading with overriding

> **Pitfall:** Compile-time types choose an overload. Assigning an argument to a wider base-type variable can select a different overload even though the runtime object remains the same subtype; this isn't dynamic polymorphism.

**Fix:** don't use a confusing overload family to simulate runtime type branching. Use `@Override` on overrides, compile a minimal example for ambiguous overloads, and inspect the static argument types at each call site.

### Breaking `equals()` and `hashCode()` together

> **Pitfall:** Overriding `equals()` for values while retaining an identity-based `hashCode()` sends equal objects to different hash buckets. Including mutable fields in both can also make an object disappear from lookup after it becomes a `HashMap` key.

**Fix:** implement and test `equals()` and `hashCode()` as a pair, using only fields that remain stable while the object serves as a key. If the domain needs both entity identity and value equality, express them with different types or explicitly named operations.

<!-- deep -->

## Object identity, equality, and lifetime

### References aren't objects

Every object has an object identity that distinguishes it from other objects. Two references can identify one object, and copying a reference doesn't copy the object. Two independent objects can also contain exactly the same field values. When one alias mutates its object, other aliases observe the same later state.

For references, `==` tests whether both operands identify the same object or are both `null`. The default `Object.equals()` also uses identity semantics, but a class can override it to define value equality. Callers should use `equals()` or `Objects.equals()` according to the type's contract, not treat wrapper caches or string interning as reasons to compare values with `==`.

Equality must be reflexive, symmetric, transitive, and consistent, and a non-null reference must not equal `null`. Equal objects must produce the same hash code; the converse isn't required. Hierarchies in which a base and subtype choose different equality fields easily break symmetry or transitivity, so value objects usually fit immutable `final` classes or records better.

| Operation | What it compares | Typical use |
| --- | --- | --- |
| `left == right` | Reference identity | Test whether these are the same object |
| `left.equals(right)` | Equality defined by the receiver | Compare values under the type contract |
| `Objects.equals(left, right)` | Null-safe delegation to `equals()` | Either operand may be `null` |
| `System.identityHashCode(value)` | A hash based as far as practical on identity | Diagnostics, not business equality |

### The construction and dispatch boundary

Object initialization proceeds through the inheritance chain, with base-constructor logic running before subtype initialization finishes. The actual object is already the subtype, so an overridable call from a constructor still dispatches into the subtype. The subtype then observes partial state, which is also why a constructor shouldn't publish `this`, register a callback, or start a thread.

Static methods belong to their declaring classes and don't participate in dynamic instance dispatch. If a base and subtype each declare the same static signature, the subtype hides the base declaration; the call expression's compile-time type selects one. Field access is likewise not virtual, and declaring same-named fields in a base and subtype merely creates two confusing pieces of state.

A downcast doesn't change an object; it asks the runtime to verify that the reference is compatible with a target type. A false assumption throws `ClassCastException`. If callers routinely accept a base type and then use `instanceof` to recover every concrete type, the common contract probably lacks an operation, or the types don't belong in one open polymorphic hierarchy.

### Ownership determines mutability risk

A `final` field guarantees only that its reference won't be reassigned; it doesn't freeze the referenced object. If a constructor stores a caller's mutable list directly, that caller can later bypass object methods and change the contents. Returning the internal list from a getter leaks the same alias in the other direction. Both routes bypass the owner's invariants.

Choose defensive-copy depth from the contract. `List.copyOf()` returns an unmodifiable list that won't reflect later structural changes to a mutable input, but it doesn't deep-copy elements and may reuse a suitable unmodifiable input; arrays require an explicit `clone()` or `Arrays.copyOf()`. When elements are themselves mutable, decide whether to share them, copy them, or replace them with immutable value types, and document that decision in the API contract.

An object's lifetime follows strong references as well. A listener, cache, executor task, or static collection can keep the object and its reachable graph alive. Encapsulation design therefore considers not only who may mutate state, but also who retains references, when registration ends, and whether resources use an explicit protocol such as `AutoCloseable` for release.

<!-- /deep -->

[Checkpoint: java/oop](https://codewiki.com/java/oop/#checkpoint)

## Further reading

- [Java Language Specification 25: Reference Types and Values](https://docs.oracle.com/javase/specs/jls/se25/html/jls-4.html#jls-4.3.1)
- [Java Language Specification 25: Classes](https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html)
- [Java Language Specification 25: Run-Time Evaluation of Method Invocation](https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.12.4.4)
- [Dev.java: Classes and Objects](https://dev.java/learn/classes-objects/)
- [Dev.java: Inheritance](https://dev.java/learn/inheritance/)
