Course topics

By WebNest Studio Engineering Team

Java - Core Tutorial

Encapsulation

Encapsulation is the practice of bundling data (fields) and the code that operates on that data (methods) into a single unit — a class — while restricting direct outside access to that data. In Java, encapsulation is achieved by declaring fields private and exposing controlled access through public getter and setter methods.

This is sometimes described as "data hiding," but its real value is control: encapsulation lets a class validate, transform, or log every read or write to its data, and lets the internal representation change later without breaking code that depends on the class.

It is one of the four pillars of OOP, alongside inheritance, polymorphism and abstraction. A good one-line summary: hide the data, expose the behavior.

Why it matters

Encapsulation is what stops other code from putting an object into a state that makes no sense — a negative width, an order with no customer. It also lets you change how a class stores its data later without breaking anything that uses it, because the outside world only ever touched the methods.

Watch: Encapsulation in Java: Complete Guide with Real Examples | Java OOP

Starts from data hiding with private fields and an access modifier recap, then covers getters and setters (and when not to write them), validation inside setters, read-only and write-only classes, immutable classes and how String works, encapsulation vs abstraction, a bank account example, and common interview questions.

Watch on YouTube

Access Modifiers Recap

Encapsulation is built on Java's four access levels, from most to least restrictive. Fields are almost always private; the methods that make up the class's public API are public.

  • private: visible only inside the same class. This is what hides the data.
  • default (no keyword): visible to classes in the same package.
  • protected: visible in the same package and to subclasses in other packages.
  • public: visible everywhere. Use it for the operations other code is meant to call.

Data Hiding with private

Marking a field private means the compiler rejects any outside access: e.salary = -100; from another class fails with "salary has private access in Employee". Outside code can then only reach the data through the methods the class chooses to offer, so the class decides what is visible, what is changeable and what values are allowed. A class whose fields are all private is sometimes called tightly encapsulated.

The Standard Pattern

A well-encapsulated class declares its fields private so no outside code can read or modify them directly, then provides public methods — conventionally named getFieldName() and setFieldName(value) — as the only way in or out. Setters can validate input before accepting it, and a class can even omit a setter entirely to make a field effectively read-only after construction.

  • Declare fields as private.
  • Provide public getter methods to read values.
  • Provide public setter methods to write values, with validation if needed.
  • Omit a setter (or make it private) for a field that should never change after construction.

Validation in Setters

A setter that only does this.salary = salary; protects nothing. The point of routing writes through a method is that the method can refuse bad values: reject a negative salary or an empty name by throwing IllegalArgumentException, so the object can never hold invalid state. Apply the same rule in the constructor (for example by calling the setter or a shared check), otherwise a bad value can still slip in when the object is created.

When NOT to Write Getters and Setters

Generating a getter and setter for every field is a habit, not a design. A public setter on every field is barely better than a public field. Instead, expose the operations the object actually supports: a BankAccount should have deposit() and withdraw() that enforce the rules, not setBalance(), which lets any caller write any balance. Leave out setters for values that should not change after creation, such as an ID, and leave out getters for data outside code has no reason to see.

Read-Only and Write-Only Classes

  • Read-only class: private fields with getters but no setters. Values are supplied once through the constructor, as with an application config or a user ID.
  • Write-only class: private fields with setters but no getters. Useful for secrets such as a password, which can be set and checked but should never be read back.

Immutable Classes

An immutable object can never change after it is constructed, which is encapsulation taken all the way. String is the best-known example: name.toUpperCase() returns a new string and leaves name untouched. Immutable objects are safe to share between threads, safe to use as HashMap keys, and can never be left half-updated. Java 16+ record types follow most of these rules automatically.

  • Declare the class final so no subclass can add mutable behavior.
  • Make every field private final and set it only in the constructor.
  • Provide no setters. To "change" a value, return a new object (for example withCourse()).
  • Make defensive copies of mutable inputs and outputs, such as lists and arrays, so outside code cannot change them behind your back. List.copyOf() returns an unmodifiable copy.

Encapsulation vs Abstraction

The two are often confused because both "hide" something. Abstraction hides complexity: it decides what an object offers, and is expressed with abstract classes and interfaces (a PaymentGateway with a pay() method). Encapsulation hides data: it decides how an object protects its state, and is expressed with private fields and controlled methods. Abstraction works at the design level; encapsulation works at the implementation level. Most well-written classes use both.

Why It Matters

Without encapsulation, any code anywhere in a program could set a field to an invalid value, such as a negative age or an empty required name, with no chance for the class to object. With encapsulation, invalid data can be rejected at a single, well-defined entry point, and the internal storage format (a single field, a computed value, a different type) can be reworked later without changing the public API that other code relies on.

Common Interview Questions

  • What is encapsulation? Bundling data and the methods that work on it into one class, and restricting direct access to the data with private.
  • Is encapsulation the same as data hiding? Data hiding (making fields private) is the mechanism; encapsulation is the wider idea of controlling all access through methods.
  • How do you achieve encapsulation in Java? Private fields, plus public methods (getters, setters or domain operations) that validate access.
  • Can a class be encapsulated without setters? Yes. A read-only or immutable class is fully encapsulated.
  • How do you make a class immutable? Make the class final, fields private final, provide no setters, and defensively copy mutable fields.
  • How can a getter break encapsulation? By returning a mutable internal object, such as a List or array, that the caller can then modify directly.
  • What is the difference between encapsulation and abstraction? Abstraction hides complexity and shows only what an object does; encapsulation hides internal data and controls how it is accessed.

Examples

Private fields, a getter, and a setter that validates

Java
public class EmployeeDemo {
    public static void main(String[] args) {
        Employee e = new Employee("Asha", 50000);
        e.setSalary(65000);
        System.out.println(e.getName() + " earns " + e.getSalary());

        try {
            e.setSalary(-100);                   // the setter guards the field
        } catch (IllegalArgumentException ex) {
            System.out.println("Rejected: " + ex.getMessage());
        }
        // e.salary = -100;                      // compile error: salary has private access
        System.out.println(e.getName() + " still earns " + e.getSalary());
    }
}

final class Employee {
    private final String name;                   // no setter: fixed after construction
    private double salary;

    Employee(String name, double salary) {
        this.name = name;
        setSalary(salary);                       // same validation for new objects
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        if (salary < 0) {
            throw new IllegalArgumentException("salary cannot be negative: " + salary);
        }
        this.salary = salary;
    }
}
Output
Asha earns 65000.0
Rejected: salary cannot be negative: -100.0
Asha still earns 65000.0

A read-only class and a write-only class

Java
public class AccessDemo {
    public static void main(String[] args) {
        AppConfig config = new AppConfig("WebNest", 8080);
        System.out.println(config.getAppName() + " on port " + config.getPort());

        PasswordBox box = new PasswordBox();
        box.setPassword("s3cret!");
        System.out.println("Correct password? " + box.matches("s3cret!"));
        System.out.println("Wrong password? " + box.matches("guess"));
    }
}

// Read-only: values come in through the constructor, getters only
class AppConfig {
    private final String appName;
    private final int port;

    AppConfig(String appName, int port) {
        this.appName = appName;
        this.port = port;
    }

    public String getAppName() { return appName; }
    public int getPort() { return port; }
}

// Write-only: the password can be set but never read back
class PasswordBox {
    private int passwordHash;

    public void setPassword(String password) {
        passwordHash = password.hashCode();      // real code would use a proper password hash
    }

    public boolean matches(String attempt) {
        return attempt.hashCode() == passwordHash;
    }
}
Output
WebNest on port 8080
Correct password? true
Wrong password? false

An immutable class with defensive copies

Java
import java.util.ArrayList;
import java.util.List;

public class ImmutableDemo {
    public static void main(String[] args) {
        List<String> courses = new ArrayList<>(List.of("Java"));
        Student s = new Student("Ravi", courses);

        courses.add("Python");                   // changing the caller's list...
        System.out.println(s.getCourses());      // ...does not reach the Student

        try {
            s.getCourses().add("SQL");           // the returned list cannot be changed either
        } catch (UnsupportedOperationException e) {
            System.out.println("Cannot modify courses");
        }

        Student updated = s.withCourse("SQL");   // "changes" produce a new object
        System.out.println(s.getCourses() + " -> " + updated.getCourses());

        String name = "java";
        name.toUpperCase();                      // String is immutable too
        System.out.println(name);
    }
}

final class Student {                            // final: no subclass can add mutable state
    private final String name;
    private final List<String> courses;

    Student(String name, List<String> courses) {
        this.name = name;
        this.courses = List.copyOf(courses);     // defensive, unmodifiable copy
    }

    public String getName() { return name; }

    public List<String> getCourses() { return courses; }

    public Student withCourse(String course) {
        List<String> next = new ArrayList<>(courses);
        next.add(course);
        return new Student(name, next);
    }
}
Output
[Java]
Cannot modify courses
[Java] -> [Java, SQL]
java

A BankAccount class enforcing rules through encapsulation

Java
public class BankAccount {
    private double balance;

    public BankAccount(double initialBalance) {
        this.balance = Math.max(initialBalance, 0);
    }

    public double getBalance() {
        return balance;
    }

    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    public boolean withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        BankAccount account = new BankAccount(100.0);
        account.deposit(50.0);
        boolean success = account.withdraw(500.0); // rejected, insufficient funds
        System.out.println("Withdraw succeeded: " + success);
        System.out.println("Balance: " + account.getBalance());
    }
}
Output
Withdraw succeeded: false
Balance: 150.0

Common Mistakes

  • Making fields public "just to save time," which removes all validation and lets any code corrupt the object's state.
  • Writing getters and setters for every field automatically without asking whether a field should be writable, or even readable, from outside the class.
  • Putting no logic in setters at all, which reduces encapsulation to a naming convention rather than real protection.
  • Returning a direct reference to a mutable private field (like an array or List) from a getter, which lets outside code modify it without going through any setter.
  • Validating in the setter but not in the constructor, so an invalid value can still enter when the object is created.
  • Adding a setBalance()-style setter where a domain operation such as deposit() or withdraw() should enforce the rules.
  • Thinking a class is immutable just because its fields are final. A final reference to a mutable List still lets the list change.

Exercise

Protect the fields of Rectangle. Width and height must be private, the setters must ignore values that are not positive, and area() returns the product.

Expected output: 12, then 12 again after an attempt to set the width to -5.

Java · starter code
class Rectangle {
    int width;
    int height;

    // TODO: make the fields private
    // TODO: a constructor taking width and height
    // TODO: setWidth(int width) that ignores values of 0 or less
    // TODO: area()
}

public class RectangleDemo {
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle(3, 4);
        System.out.println(rectangle.area());
        rectangle.setWidth(-5);
        System.out.println(rectangle.area());
    }
}
Show hints
  • Once the fields are private, the only way to change them is through methods you control.
  • Put the rule inside the setter: assign only when the new value is greater than 0.
Show solution
Java · solution
class Rectangle {
    private int width;
    private int height;

    Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    void setWidth(int width) {
        if (width > 0) {
            this.width = width;
        }
    }

    int area() {
        return width * height;
    }
}

public class RectangleDemo {
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle(3, 4);
        System.out.println(rectangle.area()); // 12
        rectangle.setWidth(-5);
        System.out.println(rectangle.area()); // 12
    }
}

Quick quiz

1. Which access modifier is normally used for the fields of an encapsulated class?

  • A. private
  • B. protected
  • C. public
  • D. No modifier
Show answer

A. Private fields can be reached only from inside the class, so all access goes through its methods.

2. A class sets its fields in the constructor and provides getters but no setters. What does that make its objects?

  • A. Write-only
  • B. Read-only after construction
  • C. Static
  • D. Abstract
Show answer

B. With no setters, callers can read the values but cannot change them once the object exists.

3. What is a practical benefit of hiding fields behind methods?

  • A. The program uses less memory
  • B. Methods run faster than field access
  • C. The internal representation can change without breaking callers
  • D. The class no longer needs a constructor
Show answer

C. Callers depend only on the methods, so you are free to rename, restructure or recalculate the data inside.

Interview questions

What is the difference between encapsulation and data hiding?

Show answer

Data hiding is the narrower idea: making fields inaccessible from outside, usually with private. Encapsulation is broader: bundling the data together with the methods that operate on it, and exposing a controlled public interface. Data hiding is one of the techniques used to achieve encapsulation.

Does adding a getter and setter for every field give you encapsulation?

Show answer

Not really. If every field has a public setter with no rules, the object is as exposed as if the fields were public. Real encapsulation means exposing only the operations that make sense, validating inputs, and not handing out references to mutable internals — for example, returning a copy of a list rather than the list itself.

Key Points to Remember

  • Encapsulation combines private fields with public getters/setters to control access.
  • It allows validation of data on write and protects internal representation from outside code.
  • A field can be made read-only by simply not providing a setter; a write-only field has a setter but no getter.
  • Immutable classes are final, have private final fields, no setters, and defensive copies of mutable data.
  • Abstraction hides complexity (what an object does); encapsulation hides data (how its state is protected).
  • Returning mutable internals directly from a getter can silently break encapsulation.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering