Encapsulation
Encapsulation is the practice of keeping an object's data private and exposing it only through controlled methods. It is one of the four pillars of OOP and the foundation of reliable, maintainable professional code.
Why Encapsulation?
When fields are public, any code anywhere in your program can set them to any value — including invalid ones. A learner's age could be set to -5, a mark to 200, a bank balance to a negative number. Encapsulation prevents this by making fields private and routing all access through methods that can validate input.
It also gives you flexibility to change your internal implementation without breaking the rest of the program — callers only see the public interface, not the internals.
Access Modifiers
| Modifier | Accessible from |
|---|---|
private | Only within the same class |
package-private (no modifier) | Within the same package |
protected | Same package and subclasses |
public | Everywhere |
Rule of thumb: fields should be private. Methods that form the public interface of the class should be public. Internal helper methods should be private.
Getters and Setters
public class Learner {
private String name;
private int age;
private double mark;
public Learner(String name, int age, double mark) {
this.name = name;
setAge(age); // use the setter — applies validation
setMark(mark);
}
// Getters — provide read access
public String getName() { return name; }
public int getAge() { return age; }
public double getMark() { return mark; }
// Setters — provide validated write access
public void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be empty.");
}
this.name = name.trim();
}
public void setAge(int age) {
if (age < 15 || age > 80) {
throw new IllegalArgumentException("Age " + age + " is not valid for a learner.");
}
this.age = age;
}
public void setMark(double mark) {
if (mark < 0 || mark > 100) {
throw new IllegalArgumentException("Mark must be 0-100. Got: " + mark);
}
this.mark = mark;
}
public void printProfile() {
System.out.printf("%-15s | Age: %2d | Mark: %5.1f%% | %s%n",
name, age, mark, getGradeLabel());
}
private String getGradeLabel() { // private — internal helper
if (mark >= 80) return "Distinction";
if (mark >= 60) return "Merit";
if (mark >= 50) return "Pass";
return "Not yet competent";
}
}
Using the Encapsulated Class
Learner l = new Learner("Thandi", 21, 74.5);
l.printProfile();
// Reading via getters:
System.out.println("Name: " + l.getName());
// Updating via setters — validation runs automatically:
l.setMark(82.0);
// These will throw exceptions:
// l.setAge(-5); // IllegalArgumentException: Age -5 is not valid
// l.setMark(150); // IllegalArgumentException: Mark must be 0-100. Got: 150
// Direct field access is now impossible from outside the class:
// l.age = -5; // Compile error: age has private access in Learner
Immutable Fields
Some fields should be set at creation and never changed — an account number, a birth date, a transaction ID. Mark these private final. No setter is provided — they can only be read.
public class BankAccount {
private final String accountNumber; // set once, never changes
private String holderName;
private double balance;
public BankAccount(String accountNumber, String holderName, double initialBalance) {
this.accountNumber = accountNumber;
this.holderName = holderName;
this.balance = initialBalance;
}
public String getAccountNumber() { return accountNumber; }
// No setAccountNumber() — account numbers do not change
public double getBalance() { return balance; }
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive.");
balance += amount;
}
public boolean withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Withdrawal must be positive.");
if (amount > balance) {
System.out.println("Insufficient funds. Balance: R" + balance);
return false;
}
balance -= amount;
return true;
}
}
Practice Task
Your Turn
Design an encapsulated Student class with private fields: studentId (String, final), firstName, lastName (both String), gpa (double, 0.0–4.0), and year (int, 1–4). Write a full constructor. Write getters for all fields. Write setters only for firstName, lastName, gpa, and year — each with appropriate validation that throws IllegalArgumentException for invalid input. Write a getFullName() method and a printTranscript() method. Create three students in main, update some fields, and print transcripts.
Common Mistakes
- Providing a setter for fields that should be immutable (like IDs) — if something should never change, omit the setter.
- Validation that only half-works — setters with no validation are worse than public fields because they give a false sense of safety.
- Not calling the setter from the constructor — the constructor bypasses its own validation. Always use
setX()in the constructor. - Making helper methods public when they are only needed internally — keep the public surface area small.
- Returning a mutable object from a getter without copying it — if a field is an array or object, returning it directly allows callers to modify it.
Professional Tip
Encapsulation is not about hiding information for its own sake — it is about owning the contract. When you control all access to your data, you can guarantee its validity, change your implementation later, and reason about your code in isolation.
Mini Quiz
What is the primary benefit of making fields private with validated setters?