Object-Oriented Programming Basics

Encapsulation & Access Modifiers

15 min Lesson 4 of 14

Encapsulation & Access Modifiers

In the previous lessons you learned how to define fields and methods on a class, and how constructors set up initial state. You have been writing public fields without much thought about it. This lesson explains why that is often a problem, and how encapsulation — one of the four pillars of OOP — solves it.

The Problem with Public Fields

Imagine a BankAccount class where the balance is a public field:

public class BankAccount { public double balance; // anyone can touch this }

Because balance is public, any code anywhere in the program can do this:

BankAccount account = new BankAccount(); account.balance = -99999.99; // perfectly legal — and totally wrong

A bank account should never allow an arbitrary outside write to set a negative balance. The field has no protection; the class cannot enforce any rules about its own data. This is the problem encapsulation is designed to prevent.

The Four Access Modifiers

Java provides four levels of visibility, from most open to most closed:

  • public — accessible from anywhere: same class, same package, subclasses, and all other code.
  • protected — accessible from the same package and from subclasses, even in other packages. Useful when designing for inheritance.
  • package-private (default) — no keyword at all. Accessible only within the same package. This is the default when you omit an access modifier.
  • private — accessible only within the class that declares it. Nothing outside — not even a subclass — can see it directly.
Rule of thumb: use private for fields by default. Loosen the modifier only when you have a clear reason — do not start with public and hope nobody abuses it.

Encapsulation: Hiding State, Exposing Behaviour

Encapsulation means bundling state (fields) and behaviour (methods) together, and hiding the internal state behind a controlled interface. Outside code calls methods; only the class itself reads or writes its own fields directly.

Here is the BankAccount rewritten with encapsulation:

public class BankAccount { private double balance; // hidden from the outside world public BankAccount(double initialBalance) { if (initialBalance < 0) { throw new IllegalArgumentException("Initial balance cannot be negative."); } this.balance = initialBalance; } public void deposit(double amount) { if (amount <= 0) { throw new IllegalArgumentException("Deposit amount must be positive."); } balance += amount; } public boolean withdraw(double amount) { if (amount <= 0 || amount > balance) { return false; // reject bad requests silently or throw — your design choice } balance -= amount; return true; } public double getBalance() { return balance; // read-only view; callers can see the value but cannot set it } }

Now compare what outside code can do:

BankAccount account = new BankAccount(500.0); account.deposit(200.0); account.withdraw(100.0); System.out.println(account.getBalance()); // 600.0 // account.balance = -99999.99; // COMPILE ERROR — 'balance' has private access

The class is now in full control of its own data. The compiler itself enforces the rules.

Where Each Modifier Applies

Access modifiers can be placed on fields, methods, constructors, and (with some restrictions) on classes. A concrete summary:

  • Fields: almost always private. Expose values through methods when needed.
  • Methods that form the public API: public.
  • Helper methods used only inside the class: private.
  • Methods shared within a package but not outside: package-private (default).
  • Methods designed to be overridden by subclasses: protected (covered in detail in the Inheritance lesson).
Start narrow, widen only if needed. It is easy to make a private method public later. But once you make something public and other code depends on it, removing that access is a breaking change. Hiding implementation details buys you the freedom to change them later without breaking callers.

Default (Package-Private) Access in Practice

When you omit the modifier entirely, the member is visible only to classes in the same package — useful for internal utility methods that should not be part of the public API but need to be shared across several classes in the same module.

class OrderValidator { // package-private class — internal detail boolean isValid(Order order) { // package-private method return order != null && order.getTotal() > 0; } }

A Complete Example: Temperature Sensor

Here is a self-contained example that uses several modifiers together:

public class TemperatureSensor { private static final double MIN_CELSIUS = -273.15; // absolute zero private double currentTemperature; private String unit; public TemperatureSensor(double initialTemp, String unit) { if (initialTemp < MIN_CELSIUS) { throw new IllegalArgumentException("Temperature below absolute zero."); } this.currentTemperature = initialTemp; this.unit = unit; } public void recordReading(double temp) { if (temp < MIN_CELSIUS) { throw new IllegalArgumentException("Invalid temperature reading."); } this.currentTemperature = temp; } public double getCurrentTemperature() { return currentTemperature; } private double toCelsius(double value) { // private helper — internal conversion logic, not part of the public API return unit.equals("F") ? (value - 32) * 5.0 / 9.0 : value; } public double getCelsius() { return toCelsius(currentTemperature); } }
  • MIN_CELSIUS is private — an internal constant; callers do not need to know it exists.
  • currentTemperature and unit are private — state hidden from the outside.
  • recordReading and getCelsius are public — the controlled interface callers use.
  • toCelsius is private — a helper that makes the implementation cleaner but is nobody else's business.
Do not expose internal helpers just to make testing easier. If a private method is difficult to test, it usually means it belongs in its own class. Changing a method from private to public solely for test access leaks implementation details and weakens encapsulation.

Summary

Encapsulation protects an object's internal state by making fields private and providing controlled access through public methods. Java's four access levels — private, package-private, protected, and public — let you precisely control what each piece of code can see. Always start with the most restrictive access and widen only when there is a clear need. In the next lesson you will turn this principle into a repeatable pattern using getters, setters, and field-level validation.