PrepZone Logo
PrepZone

Encapsulation and Abstraction

Two ideas that are constantly confused: hiding data versus hiding complexity.

Read these first

Why this matters

  • These two are routinely given as each other's definitions, and a precise answer is a quick signal of real understanding.
  • Weak encapsulation is the root cause of the bug where an object is valid when you create it and invalid an hour later.
  • Choosing the right abstraction determines how much of your codebase has to change when a requirement does.
EncapsulationHide the data, expose behaviour
AbstractionPublish intent, hide mechanics
InheritanceReuse a parent contract
PolymorphismOne call, many outcomes
Encapsulation and abstraction decide what the outside world can see. Inheritance and polymorphism decide how types relate and which code actually runs.

Encapsulation: the object guards its own state

The mechanism is simple — private fields, public methods — but the point is the invariant, the rule that must always hold.

Java
public class BankAccount {
    private final String id;
    private BigDecimal balance;

    public BankAccount(String id, BigDecimal opening) {
        if (opening.signum() < 0) {
            throw new IllegalArgumentException("Opening balance cannot be negative");
        }
        this.id = id;
        this.balance = opening;
    }

    public void deposit(BigDecimal amount) {
        requirePositive(amount);
        balance = balance.add(amount);
    }

    public void withdraw(BigDecimal amount) {
        requirePositive(amount);
        if (balance.compareTo(amount) < 0) {
            throw new InsufficientFundsException(id, amount);
        }
        balance = balance.subtract(amount);
    }

    public BigDecimal balance() {
        return balance;      // safe to return: BigDecimal is immutable
    }

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

The invariant is "balance is never negative". Because balance is private and there is no setter, that rule holds for the entire life of the object. No caller can break it, even by mistake.

The getter-and-setter habit

Generating a getter and setter for every field is the most common misunderstanding of encapsulation. The question to ask is what operation the caller actually wants.

AspectField-oriented APIOperation-oriented API
ShapegetStatus() / setStatus()approve() / reject()
Where rules liveScattered across every callerInside the class, once
Invalid statesReachable by any callerUnreachable
Reading the codeWhat does setting status to 2 mean?Obvious from the name
Changing the ruleFind every call siteEdit one method
  • Shape

    Field-oriented APIgetStatus() / setStatus()
    Operation-oriented APIapprove() / reject()
  • Where rules live

    Field-oriented APIScattered across every caller
    Operation-oriented APIInside the class, once
  • Invalid states

    Field-oriented APIReachable by any caller
    Operation-oriented APIUnreachable
  • Reading the code

    Field-oriented APIWhat does setting status to 2 mean?
    Operation-oriented APIObvious from the name
  • Changing the rule

    Field-oriented APIFind every call site
    Operation-oriented APIEdit one method

Expose what callers want to do, not the fields that happen to implement it.

Defensive copying

Private is not enough when the field holds a mutable object. Returning it hands out a live reference to your internals.

Java
public class Team {
    private final List<String> members;

    public Team(List<String> members) {
        this.members = new ArrayList<>(members);      // copy in
    }

    public List<String> members() {
        return Collections.unmodifiableList(members); // read-only view out
    }

    public void add(String member) {
        members.add(member);                           // the only way to change it
    }
}

Both directions need protecting

  • Copy on the way in. Storing the caller's list directly means they keep a reference and can modify your state afterwards.
  • Protect on the way out. Returning the field directly lets callers call clear() on it. Return an unmodifiable view or a copy.
  • Immutable types need neither. String, Integer, LocalDate, BigDecimal and records of immutable components are safe to store and return as-is.

Abstraction: hiding complexity, not data

Abstraction is about the shape of the contract. A caller should be able to use something without knowing how it works, and without changing when the how changes.

Java
// The contract: callers know only this
public interface MessageSender {
    void send(String recipient, String body);
}

// One implementation; the complexity lives here
public class EmailSender implements MessageSender {
    @Override
    public void send(String recipient, String body) {
        // connection pooling, retries, MIME encoding, bounce handling
    }
}

// Another, with completely different internals
public class SmsSender implements MessageSender {
    @Override
    public void send(String recipient, String body) {
        // gateway auth, segment splitting, delivery receipts
    }
}

// The caller: unchanged forever
public class NotificationService {
    private final MessageSender sender;

    public NotificationService(MessageSender sender) {
        this.sender = sender;
    }

    public void notifyUser(String recipient) {
        sender.send(recipient, "Your order has shipped.");
    }
}

NotificationService has no idea whether it is sending email, SMS or writing to a log in tests. Adding a third channel does not touch it.

The two side by side

AspectEncapsulationAbstraction
HidesData and internal stateImplementation and complexity
Achieved withprivate fields, public methodsinterfaces, abstract classes
Protects againstInvalid stateCoupling to one implementation
LevelInside a single classAcross a design
Question it answersWho may change this?What does a caller need to know?
  • Hides

    EncapsulationData and internal state
    AbstractionImplementation and complexity
  • Achieved with

    Encapsulationprivate fields, public methods
    Abstractioninterfaces, abstract classes
  • Protects against

    EncapsulationInvalid state
    AbstractionCoupling to one implementation
  • Level

    EncapsulationInside a single class
    AbstractionAcross a design
  • Question it answers

    EncapsulationWho may change this?
    AbstractionWhat does a caller need to know?

They usually appear together, which is why they are confused — but they solve different problems.

Package-private as a design tool

The access modifier with no keyword is under-used. It lets several classes cooperate closely while presenting a small public surface to the rest of the codebase.

Java
package com.prepzone.pricing;

public class PriceCalculator {        // the only public entry point
    public Money priceFor(Order order) {
        return new DiscountEngine().apply(order, new TaxTable().lookup(order));
    }
}

class DiscountEngine { }              // package-private: an implementation detail
class TaxTable { }                    // package-private

Other packages can only see PriceCalculator. The helpers can be renamed, merged or deleted without anyone noticing, because nothing outside could ever reference them.

Common misreadings

  • "Encapsulation is getters and setters." It is the protection of invariants. A setter for every field removes that protection entirely.
  • "Abstraction means using interfaces everywhere." An interface with exactly one implementation and no plausible second one is overhead. Abstract when a change is genuinely likely.
  • "Private fields make a class safe." Not if a getter returns a mutable collection or a getter and setter pair exposes it anyway.
  • "Abstract classes are abstraction and interfaces are not." Both provide abstraction. They differ in what they can hold, not in what they are for.

Quick recall

Everything you need if you only revisit this box.

  • Encapsulation = private data plus public operations, protecting an invariant that holds for the object's whole life.
  • A public setter for a guarded field cancels the guarantee. Expose operations, not fields.
  • Copy mutable state in, and return an unmodifiable view or copy out. Immutable types need neither.
  • unmodifiableList is a live view; List.copyOf is a snapshot.
  • Abstraction = a contract callers depend on instead of an implementation, so new implementations cost nothing.
  • Encapsulation hides data with access modifiers; abstraction hides complexity with interfaces and abstract classes.
  • Package-private keeps helper classes invisible outside their package, shrinking the public surface you must maintain.

Test yourself

Answer these before moving on — recall is what makes it stick.