Lesson 13 — Protecting Data

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

ModifierAccessible from
privateOnly within the same class
package-private (no modifier)Within the same package
protectedSame package and subclasses
publicEverywhere

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

JAVA — Encapsulated Learner
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

JAVA — Usage
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.

JAVA — Immutable fields
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?