Skip to content
Part IA Michaelmas Term

Encapsulation and Access Modifiers

What Encapsulation Is

Encapsulation has two related meanings:

  1. Bundling: grouping data (fields) together with the methods that operate on that data, inside a single class
  2. Information hiding: restricting direct access to an object’s internal representation and exposing only a controlled public interface

The first meaning is structural — it is how classes are designed. The second meaning is protective — it is why fields are private and why accessors exist.

public class BankAccount {
    private double balance;     // hidden — cannot be accessed directly

    public double getBalance() { // controlled access — read via getter
        return balance;
    }

    public void deposit(double amount) {  // controlled access — write via method
        if (amount <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        this.balance = this.balance + amount;
    }
}

External code cannot write account.balance = -1000.0 because balance is private. It must go through deposit(), which validates the input. Encapsulation enforces the class invariant.

Why Encapsulate?

Protects Invariants

A class invariant is a condition that must be true for every valid instance. For a BankAccount, balance >= 0.0 is an invariant. If balance were public, any code could set it to a negative value, breaking the invariant. With a private field and a validating setter, invalid states become unrepresentable — they cannot occur because the only path to modifying balance checks the invariant first.

Decouples Clients from Representation

Deciding to store a temperature in Celsius vs Kelvin, or a date as a String vs a LocalDate, should not affect every piece of code that uses the class. If the field is private, you can change the internal representation without changing any client code — so long as the public interface (getters, methods) stays the same.

public class Temperature {
    private double kelvin; // changed from celsius to kelvin internally

    public double getCelsius() {
        return kelvin - 273.15; // clients never knew the representation changed
    }

    public void setCelsius(double c) {
        this.kelvin = c + 273.15;
    }
}

Centralises Validation

Validation logic lives in one place — the setter — rather than duplicated everywhere a field might be modified. If the validation rules change, you update one method, not every call site.

Access Modifiers

Java provides four levels of access control:

ModifierClassPackageSubclass (any package)World
privateYesNoNoNo
(default)YesYesNoNo
protectedYesYesYesNo
publicYesYesYesYes

private

Accessible only within the same class. Use for all instance fields unless there is a compelling reason to do otherwise. Also use for helper methods that are implementation details.

private double balance;
private boolean validate(double amount) { ... }

Default (Package-Private)

No keyword — accessible from any class in the same package. Use when related classes in the same package need access but external code should not. The course will sometimes call this “package access” or “friendly access.”

class PackageHelper {        // class itself is package-private
    int internalValue;       // field is package-private
}

protected

Accessible from the same package and from subclasses (even in different packages). Use when you design a class for extension and want subclasses to access certain internals without exposing them to all clients.

protected double getInternalRate() { ... }

Protected access is a compromise: it allows subclass reuse but weakens encapsulation compared to private. A subclass in a different package can observe and modify protected members, creating coupling between the subclass and superclass implementation.

public

Accessible from everywhere. Use for the class’s public interface: constructors, public methods, and public constants. Any public member is a commitment — changing or removing it will break client code.

public static final int MAX_ATTEMPTS = 3;
public void processOrder(Order order) { ... }

Interface Members

Members declared in an interface are implicitly public. Fields in interfaces are implicitly public static final — they are constants, not instance variables. Methods are implicitly public abstract (unless default or static).

public interface Drawable {
    int DEFAULT_SIZE = 100;      // implicitly public static final
    void draw();                 // implicitly public abstract
}

You cannot make an interface method private (before Java 9), protected, or package-private — it would defeat the purpose of an interface as a public contract.

A Detailed Encapsulation Example

public class Temperature {
    private final double kelvin;
    private static final double ABSOLUTE_ZERO_CELSIUS = -273.15;

    public Temperature(double kelvin) {
        if (kelvin < 0.0) {
            throw new IllegalArgumentException(
                "Temperature cannot be below absolute zero"
            );
        }
        this.kelvin = kelvin;
    }

    public static Temperature fromCelsius(double celsius) {
        return new Temperature(celsius - ABSOLUTE_ZERO_CELSIUS);
    }

    public double getKelvin() {
        return this.kelvin;
    }

    public double getCelsius() {
        return this.kelvin + ABSOLUTE_ZERO_CELSIUS;
    }

    public double getFahrenheit() {
        return this.getCelsius() * 9.0 / 5.0 + 32.0;
    }

    public String toString() {
        return String.format("%.1f K (%.1f °C)", this.kelvin, this.getCelsius());
    }
}

The internal representation (Kelvin) is entirely private. Clients create temperatures via fromCelsius() or by passing Kelvin values. They read temperatures in any unit through getters. If the internal storage were changed from double to BigDecimal, no client code would need to change. The constructor validates the invariant — no temperature object can represent a value below absolute zero.

The Immutability Recipe

An immutable object is one whose state cannot change after construction. Immutability provides thread safety, safe sharing, and simpler reasoning. The recipe for making a class immutable:

  1. Make the class final — prevents subclassing with mutable additions
  2. Make all fields private final — prevents reassignment and external access
  3. Provide no setter methods — no way to mutate state after construction
  4. Defensively copy mutable fields in both the constructor and getters — prevents clients from modifying internal state through shared references

ImmutablePoint Example

public final class ImmutablePoint {
    private final double x;
    private final double y;

    public ImmutablePoint(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() { return this.x; }
    public double getY() { return this.y; }

    public ImmutablePoint translate(double dx, double dy) {
        return new ImmutablePoint(this.x + dx, this.y + dy);
    }
}

Notice that translate() does not modify the existing point — it returns a new ImmutablePoint. This is the functional-programming approach applied to objects. The original point is unchanged.

Defensive Copying for Mutable Fields

If a field is of a mutable type (e.g. Date, ArrayList, arrays), the immutable class must defensively copy it:

public final class Period {
    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());  // defensive copy in constructor
        this.end = new Date(end.getTime());
    }

    public Date getStart() {
        return new Date(start.getTime());        // defensive copy in getter
    }

    public Date getEnd() {
        return new Date(end.getTime());
    }
}

Without the copy in the constructor, a caller could retain a reference to the passed-in Date and mutate it after construction, changing the “immutable” Period. Without the copy in the getter, a caller could mutate the returned Date and corrupt the internal state.

This is why java.util.Date is considered a poorly designed class — it is mutable. Modern Java uses the java.time classes (LocalDate, Instant, etc.) which are immutable and do not need defensive copying.

Benefits of Immutability

BenefitExplanation
Thread-safeNo synchronisation needed — no thread can observe a change because no changes exist
Safe to share and cacheReferences to immutable objects can be freely passed around; the JVM can reuse them (e.g. string interning, Integer caching)
Simpler reasoningAn object’s value is fixed at construction — you never need to track when or where it might have been modified
Good map keys and set elementsImmutable objects have stable hash codes; mutable keys in a HashMap can become “lost” if their hash code changes after insertion

The JDK uses immutability extensively: String, the primitive wrapper classes (Integer, Double, etc.), BigInteger, BigDecimal, and the java.time classes are all immutable for these reasons.

Encapsulation and Inheritance

Inheritance can weaken encapsulation. A protected field is accessible to subclasses, which means changing it can break code in other packages. A private field with a protected getter gives somewhat more control, but the subclass still depends on the field’s existence.

The safest approach, when designing a class that will be extended: make all fields private and provide protected methods for the specific operations subclasses genuinely need. This keeps the representation hidden while allowing controlled extension.