FirstHack Learn
Log in Sign up free
Lessons in this course 0/6 All courses Object-Oriented Programming

CSE

Progress0 / 6 lessons
  1. 1. What a class models, and why OOP was invented
  2. 2. Encapsulation, and why getters exist
  3. 3. Inheritance, and when NOT to use it
  4. 4. Polymorphism, with a runnable example
  5. 5. Abstraction: interfaces versus abstract classes
  6. 6. The SOLID principles in plain English

Courses › Object-Oriented Programming

Encapsulation, and why getters exist

Private fields are not ceremony; they are how you make invalid states impossible.

11 min read · Lesson 2 of 6 · Free

The question everyone asks

"Why write private int balance and then a getBalance() that just returns it? Why not make the field public and save twenty lines?"

It is a fair question, and the usual answer — "because encapsulation" — teaches nothing. Here is the real answer.

Public fields cannot defend themselves

Java
public class BadAccount {
    public long balancePaise;

    public static void main(String[] args) {
        BadAccount a = new BadAccount();
        a.balancePaise = 50000;
        a.balancePaise = -900000;   // nothing stops this
        System.out.println(a.balancePaise);
    }
}

That compiles and runs and prints a negative balance. The class had a rule — a balance can never go below zero — and the language gave it no way to enforce it. Every place in the codebase that touches balancePaise has to remember the rule. One place forgets, and you have corrupt data with no idea which line caused it.

Private fields plus methods enforce the rule once

Java
public class BankAccount {
    private long balancePaise;

    public BankAccount(long openingPaise) {
        if (openingPaise < 0) {
            throw new IllegalArgumentException("opening balance cannot be negative");
        }
        this.balancePaise = openingPaise;
    }

    public long getBalancePaise() {
        return balancePaise;
    }

    public void deposit(long paise) {
        if (paise <= 0) {
            throw new IllegalArgumentException("deposit must be positive");
        }
        balancePaise += paise;
    }

    public void withdraw(long paise) {
        if (paise <= 0) {
            throw new IllegalArgumentException("withdrawal must be positive");
        }
        if (paise > balancePaise) {
            throw new IllegalStateException("insufficient funds");
        }
        balancePaise -= paise;
    }

    public static void main(String[] args) {
        BankAccount a = new BankAccount(50000);
        a.deposit(25000);
        a.withdraw(10000);
        System.out.println("balance = " + a.getBalancePaise() + " paise");

        try {
            a.withdraw(999999);
        } catch (IllegalStateException e) {
            System.out.println("rejected: " + e.getMessage());
        }
    }
}

Output:

Code
balance = 65000 paise
rejected: insufficient funds

Check the arithmetic: 50000 + 25000 - 10000 = 65000 paise, which is 650 rupees.

Now the rule "balance never goes negative" is true by construction. There is no code path anywhere in any file that can make it false. That guarantee is called an invariant, and holding invariants is the entire purpose of encapsulation.

Notice something else: there is no setBalance. Adding one would destroy everything, because it would let a caller set any value directly. The class exposes deposit and withdraw — operations that make sense in the problem domain — not raw field assignment.

⚠️

The most common mistake in college projects is generating a getter and a setter for every private field automatically. private plus a public setter that does no checking is exactly as unsafe as a public field, with more typing. If a setter has no validation and the field has no invariant, ask whether the setter should exist at all.

The second reason: you can change your mind later

Suppose ten files use account.balancePaise directly. Now you need the balance in rupees as well, or you need to log every read, or you want to compute the balance from a list of transactions instead of storing it.

With a public field, you edit ten files.

With a getter, you edit one:

Java
public long getBalancePaise() {
    return openingPaise + transactions.stream().mapToLong(Txn::amount).sum();
}

Every caller still writes getBalancePaise(). None of them knows or cares that the field is gone. The interface stayed the same while the implementation changed completely.

That is what people mean by "hiding implementation details". Not secrecy. Freedom to change.

Java's four access levels

Modifier Same class Same package Subclass elsewhere Anywhere
private yes no no no
(default) yes yes no no
protected yes yes yes no
public yes yes yes yes

Default rule of thumb: make every field private, and make a method public only when something outside genuinely needs to call it. Start closed and open deliberately. Starting open and closing later is a much harder conversation.

⚠️

private protects the reference, not the object it points to. If a class holds a private int[] marks and its getter returns the array itself, the caller can write into your array and change your object's state. Return a copy, Arrays.copyOf(marks, marks.length), or an unmodifiable view. This is called a leaking mutable reference and it silently breaks encapsulation in code that looks perfectly correct.

A short mental test

Before adding a getter, ask what the caller will do with the value. If the answer is "compute something and act on it", that computation probably belongs inside the class as a method.

Instead of if (account.getBalancePaise() >= price) { account.withdraw(price); } scattered everywhere, write account.pay(price) and let the class decide. Objects that expose data invite callers to make decisions on their behalf. Objects that expose behaviour keep the decisions where the rules live.