Encapsulation in Java

Encapsulation hides internal state behind a deliberate public surface, so a class can guarantee its own rules stay true.

Definition

Encapsulation means keeping the data of a class private and allowing access only through methods the class chooses to expose. The class controls its own state, and no caller can put it into an invalid condition.

Why it exists

  • Invariants hold. A rule such as "the balance is never negative" can be enforced in one place.
  • The implementation can change. Internal fields can be renamed, replaced or computed without breaking callers.
  • The surface is small. Readers only need to understand the public methods.
  • Bugs are localised. If the state is wrong, only this class could have made it wrong.

Without encapsulation

public class Account {
    public double balance;      // anyone can write anything
}

Account a = new Account();
a.balance = -50000;             // the rule is broken, and nothing stopped it

With encapsulation

public class Account {

    private double balance;

    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Deposit must be positive");
        }
        balance += amount;
    }

    public void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Withdrawal must be positive");
        }
        if (amount > balance) {
            throw new IllegalStateException("Insufficient balance");
        }
        balance -= amount;
    }

    public double getBalance() {
        return balance;
    }
}

There is no setter for balance, and that is the point. The balance changes only through operations that make sense in the domain.

Encapsulation is not "write a getter and a setter for every field". A setter for every field is a public field with extra typing. The real question is which operations the class should support.

Getters and setters, used well

public class Profile {

    private String email;

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("Invalid email address");
        }
        this.email = email;
    }
}

A setter earns its place when it validates, normalises or notifies. If it only assigns, ask whether the field should be settable at all.

Protecting mutable state

public class Team {

    private final List<String> members = new ArrayList<>();

    public void add(String member) {
        members.add(member);
    }

    // Leaks the internal list: a caller can clear it
    public List<String> getMembersUnsafe() {
        return members;
    }

    // Safe: the caller gets a read only view
    public List<String> getMembers() {
        return Collections.unmodifiableList(members);
    }
}

Returning an internal collection quietly hands out write access. Return an unmodifiable view or a defensive copy instead. The same applies to arrays, dates and any other mutable object held as a field.

Encapsulation compared with abstraction

EncapsulationAbstraction
Hides the data and the mechanismHides the complexity behind a simpler idea
Achieved with access modifiersAchieved with interfaces and abstract classes
Answers "who may touch this?"Answers "what does this offer?"
An implementation concernA design concern

They work together: abstraction chooses the surface, encapsulation defends it.

Records and immutability

public record Money(String currency, long minorUnits) {

    public Money {
        if (minorUnits < 0) {
            throw new IllegalArgumentException("Amount must not be negative");
        }
    }
}

A record exposes its components but is immutable, so there is no state to corrupt. Validation still lives in the compact constructor. For pure data carriers this is often better encapsulation than a mutable class with getters and setters.

Common mistakes

  • Generating a getter and a setter for every field automatically.
  • Returning the internal collection or array from a getter.
  • Storing a mutable object passed in by a caller without copying it.
  • Making a field protected so a subclass can use it directly, which extends the problem rather than solving it.
  • Believing private alone is enough when the field points to something mutable.

Best practices

  • Start every field private final and relax only with a reason.
  • Expose behaviour, not data: account.withdraw(500) rather than account.setBalance(...).
  • Copy mutable objects on the way in and on the way out.
  • Use records for immutable data carriers.
  • Judge a class by whether it can be broken from outside, not by how many accessors it has.

Practice

  1. Rewrite a class with public fields so its rules cannot be broken from outside.
  2. Explain how a caller could empty an internal list through a careless getter, and fix it two different ways.
  3. Which setters in a Student class are genuinely needed, and which should be constructor arguments instead?
  4. Convert a small mutable data class into a record and list what you gained and lost.
  5. Why does private final Date created still allow the created date to be changed?

Conclusion

Encapsulation is the class taking responsibility for its own correctness. Keep the fields private, expose operations rather than data, and never hand out a live reference to something the class depends on.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.