00:00

Encapsulation in Java – Data Hiding with Examples

Encapsulation is the OOP principle of bundling data with the methods that operate on it and hiding that data from outside code. Fields are made private, and the class exposes a small set of public methods that read or change them while enforcing the rules. The object becomes responsible for keeping itself valid.

This tutorial shows what goes wrong without encapsulation, how to encapsulate a class properly with validation, how to avoid leaking mutable internals, and how records make simple encapsulated classes easy to write.

1. What Is Encapsulation?

Encapsulation: data and methods wrapped together inside a class

Think of a capsule: its contents are sealed inside, and you interact only with the outside. In Java, encapsulation is achieved by:

  • declaring fields private so that only the class itself can access them;
  • providing public methods (including getters and setters where needed) that read and change the state;
  • putting validation and business rules inside those methods.

Encapsulation is often called data hiding, although strictly speaking data hiding (using private) is the mechanism and encapsulation is the broader design idea.

2. The Problem Without Encapsulation

WithoutEncapsulation.java
public class WithoutEncapsulation {
    static class Account {
        public String owner;
        public double balance;     // anyone can write anything
    }

    public static void main(String[] args) {
        Account acc = new Account();
        acc.owner = "";            // invalid owner
        acc.balance = -50_000;     // invalid balance, no checks possible
        System.out.println("Owner: '" + acc.owner + "', balance: " + acc.balance);
    }
}
Output
Owner: '', balance: -50000.0

With public fields, the class cannot protect itself. Every piece of code that uses Account must remember the rules, and one mistake anywhere produces invalid data.

3. Encapsulating a Class

EncapsulatedAccount.java
public class EncapsulatedAccount {
    static class Account {
        private final String owner;     // hidden, and fixed after creation
        private double balance;         // hidden; changed only through methods

        Account(String owner, double opening) {
            if (owner == null || owner.isBlank()) throw new IllegalArgumentException("owner is required");
            if (opening < 0) throw new IllegalArgumentException("opening balance cannot be negative");
            this.owner = owner;
            this.balance = opening;
        }

        public String getOwner()   { return owner; }      // read-only property
        public double getBalance() { return 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 funds");
            balance -= amount;
        }
    }

    public static void main(String[] args) {
        Account acc = new Account("Asha", 1_000);
        acc.deposit(500);
        acc.withdraw(300);
        System.out.println(acc.getOwner() + " has " + acc.getBalance());

        try { acc.withdraw(5_000); }
        catch (IllegalStateException e) { System.out.println("Rejected: " + e.getMessage()); }
        try { acc.deposit(-10); }
        catch (IllegalArgumentException e) { System.out.println("Rejected: " + e.getMessage()); }
        // acc.balance = 1_000_000;   // compile error: balance has private access
        System.out.println("Balance still " + acc.getBalance());
    }
}
Output
Asha has 1200.0
Rejected: insufficient funds
Rejected: deposit must be positive
Balance still 1200.0
  • The constructor guarantees every Account starts in a valid state.
  • There is no setBalance(): the balance changes only through meaningful operations, deposit and withdraw, which enforce the rules.
  • owner is final with only a getter, so it is read-only.

4. Getters and Setters

By convention, a property name is read with getName() (or isActive() for booleans) and written with setName(...). Frameworks such as Spring, Hibernate and Jackson rely on these JavaBeans naming conventions.

ProvideResult
Getter and setterRead-write property (setter should validate)
Getter onlyRead-only property
Setter onlyWrite-only property (rare; e.g. a password)
NeitherFully internal state

Do not generate a getter and setter for every field automatically. A public setter without checks is barely better than a public field. Prefer methods that express behaviour (withdraw, activate, rename).

5. Changing Internals Without Breaking Callers

Because callers depend only on public methods, you can change the internal representation freely:

ChangeInternals.java
public class ChangeInternals {
    // Version 2 stores the temperature in Kelvin internally.
    // The public API (Celsius getters/setters) did not change, so no caller breaks.
    static class Thermostat {
        private double kelvin = 293.15;

        public double getCelsius() { return kelvin - 273.15; }
        public void setCelsius(double celsius) {
            if (celsius < -273.15) throw new IllegalArgumentException("below absolute zero");
            kelvin = celsius + 273.15;
        }
        public double getFahrenheit() { return getCelsius() * 9 / 5 + 32; }   // new feature, free
    }

    public static void main(String[] args) {
        Thermostat t = new Thermostat();
        t.setCelsius(25);
        System.out.printf("%.1f C = %.1f F%n", t.getCelsius(), t.getFahrenheit());
    }
}
Output
25.0 C = 77.0 F

The thermostat now stores Kelvin, yet every caller still works with Celsius, and adding Fahrenheit support took one method.

6. Do Not Leak Mutable Internals

A private field is not truly hidden if a getter returns the mutable object itself. Copy mutable data on the way in and on the way out:

DefensiveCopy.java
import java.util.ArrayList;
import java.util.List;

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

        Team(List<String> members) {
            this.members = new ArrayList<>(members);       // copy in
        }
        public List<String> getMembers() {
            return List.copyOf(members);                    // read-only copy out
        }
        public void addMember(String name) {
            if (members.size() >= 3) throw new IllegalStateException("team is full");
            members.add(name);
        }
    }

    static class LeakyTeam {
        private final List<String> members = new ArrayList<>();
        public List<String> getMembers() { return members; } // leaks internal list!
    }

    public static void main(String[] args) {
        LeakyTeam leaky = new LeakyTeam();
        leaky.getMembers().add("Intruder");                  // bypasses any rules
        System.out.println("Leaky team: " + leaky.getMembers());

        List<String> input = new ArrayList<>(List.of("Asha", "Ravi"));
        Team team = new Team(input);
        input.add("Sneaky");                                 // does not affect team
        team.addMember("Meera");
        System.out.println("Team: " + team.getMembers());
        try {
            team.getMembers().add("Intruder");
        } catch (UnsupportedOperationException e) {
            System.out.println("Cannot modify the returned list: " + e.getClass().getSimpleName());
        }
    }
}
Output
Leaky team: [Intruder]
Team: [Asha, Ravi, Meera]
Cannot modify the returned list: UnsupportedOperationException

The same applies to arrays, Date objects and any other mutable types. Immutable types such as String, LocalDate and records of immutable values can be shared safely.

7. Records: Encapsulation with Less Code

For classes that just carry data, a record (Java 16+) generates private final fields, accessors, equals, hashCode and toString. A compact constructor adds validation:

RecordEncapsulation.java
public class RecordEncapsulation {
    // Records: private final fields, accessors, equals/hashCode/toString generated
    record Email(String value) {
        Email {                                   // compact constructor validates input
            if (value == null || !value.matches("[^@\\s]+@[^@\\s]+\\.[a-z]{2,}"))
                throw new IllegalArgumentException("invalid email: " + value);
            value = value.toLowerCase();
        }
    }

    public static void main(String[] args) {
        Email e = new Email("Asha@JavaByTechie.com");
        System.out.println(e.value());
        System.out.println(e);
        try { new Email("not-an-email"); }
        catch (IllegalArgumentException ex) { System.out.println(ex.getMessage()); }
    }
}
Output
asha@javabytechie.com
Email[value=asha@javabytechie.com]
invalid email: not-an-email

8. Levels of Encapsulation

Access modifiers control encapsulation at several levels:

  • Class level: private members are visible only inside the class.
  • Package level: members with no modifier are visible only within the package, useful for helper classes.
  • Module level: since Java 9, a module exports only selected packages; everything else is hidden from other modules.

See Access Modifiers in Java.

9. Encapsulation vs Abstraction

AspectEncapsulationAbstraction
FocusProtecting an object's data and invariantsHiding implementation complexity behind a simple contract
Question it answersWho may access or change this state?What does this object do, regardless of how?
Java toolsAccess modifiers, getters/setters, immutabilityInterfaces, abstract classes
LevelImplementation levelDesign level

10. Benefits

  • Data integrity: objects cannot be put into invalid states.
  • Flexibility: internals can change without affecting callers.
  • Maintainability: rules live in one place instead of being repeated.
  • Security and thread-safety: controlled access makes it easier to add checks, logging or synchronization.
  • Testability: a small public API is easier to test.

11. Summary

  • Encapsulation bundles data with behaviour and hides data behind private.
  • Expose meaningful methods that validate input instead of raw setters.
  • Make fields final and omit setters for read-only or immutable classes.
  • Copy mutable objects on the way in and out so internals cannot leak.
  • Records give concise, encapsulated data classes.

Related Tutorials