Encapsulation in Java

By the Syncfusion DevTutorials team · Last updated

Protect object data with private fields, getters and setters, validation and read-only patterns, and see how encapsulation differs from data hiding.

Think about a bank locker.

Your valuables sit inside a locked box. You cannot reach in and take something out. You go to the counter, show your ID, and the bank checks it. Only then do you get access.

The bank does not stop you from using your locker. It just makes sure every visit goes through a proper check.

Encapsulation works the same way. An object locks its data away, and other code can only reach that data through the object's own methods. Those methods can check every request.

In simple words: encapsulation keeps an object's data private and lets other code use it only through safe methods.

What You Will Learn

In this lesson, you will learn:

  1. What encapsulation is, and why it protects your objects.
  2. How data hiding with private works.
  3. How access modifiers control what others can reach.
  4. How to write getters and setters.
  5. How setters can reject invalid values.
  6. How to make data read-only.
  7. The benefits of encapsulation.
  8. How encapsulation differs from data hiding.

Start With a Working Example

Press Run.

The account refuses a bad withdrawal

public class Main {
    public static void main(String[] args) {
        BankAccount acc = new BankAccount("Asha", 1000);

        acc.deposit(500);
        acc.withdraw(300);
        acc.withdraw(5000);
        acc.deposit(-200);

        System.out.println("Final balance: " + acc.getBalance());
    }
}

class BankAccount {
    private String owner;
    private int balance;

    BankAccount(String owner, int balance) {
        this.owner = owner;
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public void deposit(int amount) {
        if (amount <= 0) {
            System.out.println("Deposit refused: amount must be positive.");
            return;
        }
        balance += amount;
        System.out.println("Deposited " + amount);
    }

    public void withdraw(int amount) {
        if (amount > balance) {
            System.out.println("Withdrawal refused: not enough money.");
            return;
        }
        balance -= amount;
        System.out.println("Withdrew " + amount);
    }
}

Expected output:

Deposited 500
Withdrew 300
Withdrawal refused: not enough money.
Deposit refused: amount must be positive.
Final balance: 1200

balance is private. The only ways to change it are deposit and withdraw, and both check the amount first. So the balance can never go below zero, and nobody can deposit a negative amount.

Try This

In main, add the line acc.balance = 9999999;. The program no longer compiles. Outside code cannot touch the balance directly.

Introduction to Encapsulation

Encapsulation means bundling an object's data and the methods that work on that data into one class, and controlling access from outside.

It is one of the four main ideas of object-oriented programming:

Encapsulation = Protect data
Abstraction   = Hide details
Inheritance   = Is a
Polymorphism  = Many behaviours

Without encapsulation, any code anywhere could change any field to anything. One wrong line, like student.age = -5;, would put an object into an impossible state. Finding that line in a big program is hard.

With encapsulation, every change goes through the object's own methods. The object can say "no" to bad values. So the object always stays valid.

Data Hiding

Data hiding means making fields private, so code outside the class cannot read or change them directly.

Outside code cannot reach a private field

public class Main {
    public static void main(String[] args) {
        Student s = new Student();
        s.age = -5;
        System.out.println(s.age);
    }
}

class Student {
    private int age;
}

The compiler reports age has private access in Student.

The impossible age -5 never gets in. The mistake is caught before the program even runs.

Hiding the data is the first step. The next step is giving outside code a safe way in.

Access Control

You already know the four access levels. In encapsulation, each one has a typical job:

ModifierWho can use itTypical use in encapsulation
privateOnly this classFields, and helper methods
default (none)Classes in the same packageHelpers shared inside one package
protectedSame package, plus child classesMembers child classes need to customise
publicEveryoneThe methods other code is meant to call

The usual pattern is simple:

  • Fields: private.
  • Methods other code needs: public.
  • Internal helper methods: private.

Easy Way to Remember: data private, actions public. Open up a member only when another class has a real need for it.

Getters and Setters

If a field is private, how does other code read or change it? Through two kinds of public methods:

  • A getter returns a field's value. Its name starts with get, such as getName().
  • A setter changes a field's value. Its name starts with set, such as setName(String name).

For a boolean field, the getter usually starts with is, such as isActive().

Read and change private fields through methods

public class Main {
    public static void main(String[] args) {
        Product p = new Product();

        p.setName("Wireless Mouse");
        p.setPrice(899);
        p.setInStock(true);

        System.out.println(p.getName());
        System.out.println("Price: " + p.getPrice());
        System.out.println("In stock? " + p.isInStock());
    }
}

class Product {
    private String name;
    private int price;
    private boolean inStock;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public boolean isInStock() {
        return inStock;
    }

    public void setInStock(boolean inStock) {
        this.inStock = inStock;
    }
}

Expected output:

Wireless Mouse
Price: 899
In stock? true

Every field is private. main reads and changes them only through the getters and setters.

At first, this can look like extra work for no gain. The gain comes in the next section: once every change goes through a setter, the setter can check it.

FieldGetterSetter
private String name;public String getName()public void setName(String name)
private int price;public int getPrice()public void setPrice(int price)
private boolean inStock;public boolean isInStock()public void setInStock(boolean inStock)

Property Validation

Validation means checking a value before accepting it. A setter is the perfect place, because it is the only door into the field.

A setter can:

  • reject a bad value and keep the old one;
  • correct a value, such as trimming spaces;
  • throw an error, so the caller knows they made a mistake.

Setters reject values that make no sense

public class Main {
    public static void main(String[] args) {
        Student s = new Student();

        s.setName("  Meena  ");
        s.setAge(16);
        s.setMarks(88);

        s.setAge(-3);
        s.setMarks(140);
        s.setName("");

        System.out.println(s.getName() + ", age " + s.getAge() + ", marks " + s.getMarks());
    }
}

class Student {
    private String name = "Unknown";
    private int age;
    private int marks;

    public String getName() { return name; }
    public int getAge() { return age; }
    public int getMarks() { return marks; }

    public void setName(String name) {
        if (name == null || name.isBlank()) {
            System.out.println("Name refused: it cannot be empty.");
            return;
        }
        this.name = name.trim();
    }

    public void setAge(int age) {
        if (age < 0 || age > 120) {
            System.out.println("Age refused: " + age);
            return;
        }
        this.age = age;
    }

    public void setMarks(int marks) {
        if (marks < 0 || marks > 100) {
            System.out.println("Marks refused: " + marks);
            return;
        }
        this.marks = marks;
    }
}

Expected output:

Age refused: -3
Marks refused: 140
Name refused: it cannot be empty.
Meena, age 16, marks 88

The three bad values were all refused. The object kept its last good values.

setName also trimmed the extra spaces from " Meena ". Callers did not have to remember to do that.

Throwing an Error Instead

Printing a message is fine for learning. In real programs, a setter usually throws an exception, so the mistake cannot be ignored:

public void setAge(int age) {
    if (age < 0 || age > 120) {
        throw new IllegalArgumentException("Age must be 0 to 120");
    }
    this.age = age;
}

throw stops the current method with an error. You will learn how to handle errors in the Exception Handling chapter.

Validate in the Constructor Too

A constructor is another door into the object. If it sets fields directly, it can let bad values in. A simple fix is to have the constructor call the setters.

Student(String name, int age) {
    setName(name);
    setAge(age);
}

Read-Only Data Patterns

Some data should be set once and never changed. An order ID, a date of birth or an account number are good examples.

There are three common ways to make data read-only.

1. Getter With No Setter

Provide a getter, but no setter. Outside code can read the value, but cannot change it.

2. final Fields Set in the Constructor

Mark the field final. It must be set in the constructor, and it can never be changed again, even by the class's own methods.

3. A Record

A record makes every field final and read-only automatically. You learned records in the Enums lesson.

An ID that can be read but never changed

public class Main {
    public static void main(String[] args) {
        Order o = new Order("ORD-2041", "Asha");

        System.out.println("Order ID: " + o.getId());
        System.out.println("Customer: " + o.getCustomer());

        o.setStatus("SHIPPED");
        System.out.println("Status: " + o.getStatus());

        Receipt r = new Receipt("ORD-2041", 1499);
        System.out.println("Receipt: " + r.orderId() + " paid " + r.amount());
    }
}

class Order {
    private final String id;
    private final String customer;
    private String status = "PLACED";

    Order(String id, String customer) {
        this.id = id;
        this.customer = customer;
    }

    public String getId() { return id; }
    public String getCustomer() { return customer; }

    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
}

record Receipt(String orderId, int amount) {
}

Expected output:

Order ID: ORD-2041
Customer: Asha
Status: SHIPPED
Receipt: ORD-2041 paid 1499

id and customer are final and have only getters. They are fixed for the life of the order.

status has a getter and a setter, because an order's status really does change.

Receipt is a record, so both of its values are read-only with no extra code.

Protecting Arrays and Lists

A getter that returns an array hands out a reference to the original. Outside code could then change its contents.

To stay safe, return a copy:

public int[] getMarks() {
    return Arrays.copyOf(marks, marks.length);
}

Now outside code gets its own copy. Changing it does not change the object's data.

final on an array field only stops the field from pointing to a different array. The values inside the array can still be changed. Return copies from getters to keep the contents safe.

PatternWho can change the valueExample
Getter and setterAnyone, through the setter's checksstatus
Getter onlyOnly the class's own methodsA running total
final field with a getterNobody, after the constructorAn order ID
Record componentNobody, everA receipt

Not every field needs a setter. Add a setter only when outside code has a real reason to change the value. Fewer setters means fewer ways for an object to end up in a bad state.

Benefits of Encapsulation

Valid Objects

Setters and methods check every change. An object can never end up with a negative age or a balance below zero.

Freedom to Change the Inside

Other code only uses the public methods. You can change how data is stored, as long as the methods still work the same way.

For example, a class could switch from storing age to storing dateOfBirth. Its getAge() method would just work out the age. Nobody using the class would notice.

Easier Debugging

If a value goes wrong, there is only one place it can be changed: the setter or method. You put one check or one print statement there.

Control Over Access

You decide what is read-only, what is changeable and what is completely hidden.

Clear, Simple Use

Other programmers see a small set of public methods. They do not need to understand the inner details.

Encapsulation vs Data Hiding

The two terms are often used as if they were the same. They are closely related, but they are not identical.

  • Data hiding is one technique: making fields private, so outside code cannot reach them.
  • Encapsulation is the bigger idea: grouping data with the methods that manage it, and controlling access through those methods. Data hiding is one part of it.
Data hidingEncapsulation
What it isA techniqueA design principle
Main focusKeeping data out of reachBundling data with the methods that protect it
Achieved withprivate fieldsprivate fields plus public getters, setters and methods
Answers"Who can see this field?""How should this object's data be used?"
RelationshipOne part of encapsulationIncludes data hiding

Think of a medicine capsule. The shell keeps the medicine hidden: that is data hiding. The capsule as a whole, a sealed package with the right dose inside, designed to be taken in one safe way, is encapsulation.

Common mistakes

The following mistakes are common when hiding fields behind getters and setters. Each row shows what you see, why it happens and how to fix it.

SymptomCauseFix
Fields hold impossible valuesFields are public, or setters skip checksMake fields private and validate in setters
A setter seems to do nothingname = name; without thisWrite this.name = name;
has private accessOutside code used a private fieldUse the getter or setter
cannot assign a value to final variableA setter tried to change a final fieldRemove the setter
An object's array changes unexpectedlyA getter returned the original arrayReturn a copy
Bad values get in through newThe constructor sets fields directlyCall the setters from the constructor

What to Remember

  • Encapsulation bundles data with the methods that manage it, and controls access from outside.
  • Data hiding makes fields private. It is one part of encapsulation.
  • Getters read private fields. Setters change them. Boolean getters usually start with is.
  • Setters are the place to validate values, so objects always stay valid.
  • Make data read-only with a getter and no setter, a final field, or a record. Return copies of arrays.
  • Encapsulation keeps objects valid, makes code easier to change and debug, and gives you control over access.

Next: Exception Handling