skip to content

Java's OOP Model

How the four OOP pillars map onto concrete Java features: access modifiers for encapsulation, extends plus interfaces for inheritance, dynamic dispatch for polymorphism, abstract types for abstraction. A common opening question, and the answer that shows you understand why Java allows single inheritance of implementation but multiple inheritance of type.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

How does Java implement encapsulation, and why is hiding fields behind accessors more than just a convention?

level: juniorimportance: must knowfreq 80%

answer

  1. private fields + public getters/setters
  2. JavaBeans: getX / isX / setX
  3. Setter = choke point for validation and invariants
  4. Representation independence: callers depend on methods, not fields
  5. Immutable = final class, final fields, no setters, defensive copies

basics

~20 s

Java hides an object's data by marking fields private and exposing them through public getter and setter methods. This lets the class control and validate access and change its internals later without breaking other code.

solid answer

~40 s

Encapsulation in Java means bundling state with behavior and restricting direct access to the state. The mechanism is access modifiers: fields are typically private, so no outside code can read or write them directly; access goes through public accessor methods (getters/setters), following the JavaBeans naming convention (getX, isX for booleans, setX). This buys you three things: validation (a setter can reject bad values), invariant maintenance (the object stays in a valid state), and freedom to change the representation later without breaking callers, since they only depend on the method surface. For truly immutable design you go further: drop setters, make fields final, and defensively copy mutable inputs. Encapsulation is the basis for maintainable APIs because it draws a clear line between a class's public contract and its private implementation.

code

java · 18 lines
java
public final class BankAccount {
    private long balanceCents; // hidden state

    public BankAccount(long initialCents) {
        if (initialCents < 0) throw new IllegalArgumentException("negative");
        this.balanceCents = initialCents;
    }

    public long getBalanceCents() {      // getter
        return balanceCents;
    }

    public void deposit(long cents) {    // controlled mutation, enforces invariant
        if (cents <= 0) throw new IllegalArgumentException("must be positive");
        balanceCents += cents;
    }
    // No setBalance: balance can only change through valid operations.
}

go deeper

for a junior

Knows to make fields private and expose getters/setters, and can state that this controls access.

for a middle

Explains the full access-modifier matrix and uses setters for validation; recognizes the reference-leak problem with mutable getters.

for a senior

Designs for representation independence and immutability (final fields, defensive copies, unmodifiable views) and avoids reflexive getter/setter generation.

for a principal

Treats encapsulation as API/module boundary design — minimizing public surface, controlling invariants across a subsystem, and choosing package-private vs public to enforce architectural layering.

## The problem encapsulation solves Imagine a `BankAccount` whose balance is a plain public field. Any code anywhere could write `account.balance = -1000`, putting the object in an impossible state. There is no single place to enforce the rule *"balance must never go negative"*, and if you later decide to store the balance in cents instead of dollars, every caller breaks. **Encapsulation** is the OOP principle that fixes this: an object should **hide its internal data** and expose only a controlled set of operations. ## The Java mechanism: access modifiers Java enforces encapsulation with **access modifiers** — keywords that control where a member is visible: | Modifier | Same class | Same package | Subclass (other package) | Everywhere | |---|---|---|---|---| | `private` | yes | no | no | no | | *(default)* package-private | yes | yes | no | no | | `protected` | yes | yes | yes | no | | `public` | yes | yes | yes | yes | The encapsulation idiom is: **make fields `private`** and provide **`public` accessor methods**: - a **getter** (`public int getBalance()`) returns the value; - a **setter** (`public void setBalance(int v)`) sets it, optionally validating. ## JavaBeans naming convention The getter/setter names follow the **JavaBeans** convention so tools and frameworks (Spring, Jackson, etc.) can discover properties reflectively: - read a property `x` → `getX()`; for a `boolean`, `isX()` is also allowed; - write a property `x` → `setX(value)`. A "property" is thus a *concept*, not necessarily a field — `getFullName()` can compute from two fields. ## Why it is more than convention 1. **Validation / invariants.** A setter is the single choke point to reject illegal values (`if (v < 0) throw ...`). The object can guarantee it is never in an invalid state — an *invariant*. 2. **Representation independence.** Callers depend only on the method signatures, not the field layout. You can switch the internal type, cache a derived value, or compute on the fly without touching callers. 3. **Read/write asymmetry.** You can expose a getter but no setter (read-only), or validate writes more strictly than reads. 4. **Security and safety.** Hiding mutable internals prevents outside code from corrupting state. For collections or dates you return *defensive copies* so callers can't mutate your internals through the reference you handed out. ## Taking it further: immutability The strongest form of encapsulation is an **immutable class**: mark the class `final` (no subclass can add mutable state), make every field `private final`, provide no setters, and **defensively copy** any mutable field both on input (constructor) and output (getter). A **record** automates much of this — each component becomes a `private final` field with an accessor — though records still need a compact constructor to defensively copy mutable components. ## Common misunderstanding Encapsulation is *not* the same as abstraction. Abstraction is about hiding *complexity* behind a simple interface (what a thing does); encapsulation is specifically about hiding *data/state* and controlling access to it. They reinforce each other but are distinct pillars.

  • Why might returning a private List field directly from a getter break encapsulation?
    Because the caller receives a reference to your internal list and can add or remove elements, mutating your object's state behind its back. You should return an unmodifiable view (Collections.unmodifiableList) or a defensive copy.
  • Does making a field private guarantee an object is immutable?
    No. A private field can still be reassigned by the class's own methods (e.g. a setter). Immutability also requires no mutation operations, final fields, and defensive copies of any mutable referenced objects.

A vending machine: you can't reach inside to grab a soda or rearrange the shelves (private state). You interact only through buttons and a coin slot (public methods), which enforce the rules — pay first, then dispense.

saying these in an interview costs you the question

  • Equating encapsulation with simply writing getters/setters for every field — auto-generated public setters for all fields actually weaken encapsulation.
  • Returning direct references to mutable internal collections/arrays from getters (reference leak).
  • Confusing encapsulation (hides state) with abstraction (hides complexity).
  • Thinking package-private means 'no access control' — it still restricts to the same package.

context

open as a page

What are the four pillars of object-oriented programming, and how does Java realize each one in concrete language features?

level: juniorimportance: must knowfreq 88%

basics

~10 s

The four pillars are encapsulation, inheritance, polymorphism, and abstraction. Java uses access modifiers for encapsulation, extends and interfaces for inheritance, method overriding for polymorphism, and abstract classes and interfaces for abstraction.

open as a page

Explain dynamic method dispatch (late binding) in Java: what is bound at compile time versus runtime, and how does it differ from overloading?

level: middleimportance: must knowfreq 78%

basics

~20 s

When you call an overridden method, Java decides at runtime which version to run based on the object's actual type, not the variable's declared type. This is dynamic dispatch. Overloading is different: the compiler picks among same-named methods at compile time using the argument types.

open as a page

In Java's OOP model, when would you choose an abstract class over an interface (or vice versa), and what does each let you do that the other cannot?

level: middleimportance: should knowfreq 74%

basics

~20 s

Use an interface to define a pure capability that many unrelated classes can implement, since a class can implement many interfaces. Use an abstract class when you need shared state (fields) or a partial implementation, since a class can extend only one.

open as a page

Java enforces single inheritance of implementation but multiple inheritance of type. What problems does this design solve and create, and how do modern Java features and principles like 'composition over inheritance' address its limits?

level: seniorimportance: should knowfreq 55%

basics

~20 s

Allowing a class to extend only one class avoids ambiguity when two parents have conflicting code (the diamond problem), while letting a class implement many interfaces keeps flexibility. The downside is rigid hierarchies, which Java teams usually solve by favoring composition (holding objects and delegating) over deep inheritance.

open as a page