Skip to content
Java core 3 min read

Encapsulation

Encapsulation bundles data with the methods that operate on it and restricts direct access to that data. By hiding internal state behind a controlled interface, you protect invariants, localize change, and make objects safe to evolve. It is the discipline that turns a bag of public fields into a robust, self-governing type.

Private Fields with Getters and Setters

The core technique: make fields private and mediate access through methods. This lets you validate input, derive values, and change the internal representation without breaking callers.

public class Temperature {
    private double celsius;

    public double getCelsius() { return celsius; }

    public void setCelsius(double celsius) {
        if (celsius < -273.15) {
            throw new IllegalArgumentException("Below absolute zero");
        }
        this.celsius = celsius;
    }

    // Derived value—no backing field needed
    public double getFahrenheit() {
        return celsius * 9 / 5 + 32;
    }
}
Temperature t = new Temperature();
t.setCelsius(25);
System.out.println(t.getFahrenheit());

Output:

77.0

The setter enforces a physical invariant; callers cannot place the object into an impossible state.

Note: A getter/setter pair is not automatically encapsulation. Blindly exposing every field through accessors is just public state in disguise. Expose only what the object’s contract genuinely needs.

Immutability

The strongest form of encapsulation is making objects immutable: once constructed, their state never changes. Immutable objects are inherently thread-safe, freely shareable, and trivially cacheable.

To make a class immutable:

  1. Mark the class final so it cannot be subclassed.
  2. Make all fields private final.
  3. Provide no setters.
  4. Defensively copy mutable inputs and outputs.
public final class Money {
    private final long amount;
    private final String currency;

    public Money(long amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public long amount()      { return amount; }
    public String currency()  { return currency; }

    public Money plus(Money other) {
        return new Money(this.amount + other.amount, currency); // new instance
    }
}

Warning: A final reference field still points at a mutable object. If you store a List, copy it on the way in and return an unmodifiable view, or callers can mutate your internals.

Records (Java 16+)

A record is a concise, modern way to declare an immutable, transparent data carrier. The compiler generates the constructor, private final fields, accessors, equals, hashCode, and toString automatically.

public record Point(int x, int y) {
    // Compact constructor for validation
    public Point {
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException("Coordinates must be non-negative");
        }
    }
}
Point p = new Point(3, 4);
System.out.println(p);
System.out.println(p.x());
System.out.println(p.equals(new Point(3, 4)));

Output:

Point[x=3, y=4]
3
true

Records encapsulate by default: their fields are private and final, and they expose read-only accessors. They are the idiomatic choice for DTOs, value objects, and any immutable aggregate.

Tip: Records implement equals/hashCode by value, making them perfect as keys in maps and elements in sets.

Best Practices

  • Default fields to private; widen access only with a concrete justification.
  • Favor immutability—use final fields and avoid setters wherever the domain allows.
  • Validate in constructors and setters so an object is never observably invalid.
  • Defensively copy mutable collections and arrays at boundaries.
  • Use records for immutable value types instead of hand-writing boilerplate.
  • Return unmodifiable views (List.copyOf, Collections.unmodifiableList) from getters that expose collections.
Last updated June 1, 2026
Was this helpful?