Guarding your fields
In lesson 5-1, anyone could write rex.age = -50. In a single-file program that is survivable. In a codebase where forty other classes touch your BankAccount, one careless write from anywhere corrupts data that every other feature trusts.
Encapsulation closes that door. Mark fields private so only code inside the class can touch them, then expose controlled access through methods:
class BankAccount { private int balance = 0; void deposit(int amount) { if (amount > 0) { balance += amount; } else { System.out.println("deposit rejected"); } } int getBalance() { return balance; } }
Now every change to balance passes through deposit, which enforces the rules in one place. A method that reads a field is a getter, such as getBalance, and one that writes it with validation is a setter, such as setName.
Outside code writing account.balance = 999 no longer compiles, so the invalid state cannot even be expressed.
A class that defends its own balance
One valid deposit, one invalid deposit, and a readable printed form.
public class Main { public static void main(String[] args) { BankAccount account = new BankAccount(); account.deposit(100); account.deposit(-50); System.out.println("balance: " + account.getBalance()); System.out.println(account); } } class BankAccount { private int balance = 0; void deposit(int amount) { if (amount > 0) { balance += amount; } else { System.out.println("deposit rejected"); } } int getBalance() { return balance; } @Override public String toString() { return "BankAccount[balance=" + balance + "]"; } }
Output
deposit rejected balance: 100 BankAccount[balance=100]
The negative deposit was refused by the class itself, so the balance stayed correct without main checking anything. The rule lives with the data it protects.
The last line passes the object straight to println, and Java calls the object's toString to decide what to print. That method is the subject of the next section.
toString and access levels
toString() is Java's __str__. Override it and printing an object shows something readable instead of BankAccount@1b6d3586, which is the class name and a memory-derived number.
The @Override annotation asks the compiler to verify that you really are replacing an inherited method, which catches typos such as tostring that would otherwise compile as a new unrelated method.
One more term is needed before the access table makes sense. A package is Java's way of grouping related classes under a shared name, the way folders group files. Each class declares its package at the top of the file, as in package com.shop.billing;, and classes in the same package sit in the same folder of the project.
The standard library ships packages such as java.util, where the collections live. That is what a line like import java.util.ArrayList; refers to, and unit 7 puts it to work.
The access keywords you now know:
| Keyword | Who can access |
|---|---|
public | any code anywhere |
private | only inside the same class |
| none written | any code in the same package |
House style is fields private and methods public unless they are internal helpers. That keeps every object in charge of its own valid state, which is the core promise of OOP.
What private actually blocks
When a field is declared private int balance, code in another class that writes account.balance = 999 does not compile.
private means only code inside BankAccount itself can read or write the field. The compiler rejects the access from outside, so the invalid state cannot be expressed at all, and this is encapsulation enforced at compile time rather than checked at runtime.
The restriction is per class, not per object. A method inside BankAccount may freely read other.balance on a different BankAccount, which is how a transferTo method can be written.
Note also what private does not do. It is an organizing rule for source code, not a security boundary, so it protects your invariants from accidents rather than protecting secrets from an attacker.
A Player with validated scoring
Private fields, a constructor, a guarded mutator, getters, and a printed form.
public class Main { public static void main(String[] args) { Player p = new Player("Ada"); p.addPoints(10); p.addPoints(-3); p.addPoints(5); System.out.println(p); } } class Player { private String name; private int score = 0; Player(String name) { this.name = name; } void addPoints(int p) { if (p > 0) { score += p; } } String getName() { return name; } int getScore() { return score; } @Override public String toString() { return name + ": " + score; } }
Output
Ada: 15addPoints guards with if (p > 0) before touching score, so the 10 and the 5 land while the -3 is ignored, leaving 15.
toString has to be public String toString() exactly, since it is replacing an inherited method, and the @Override above it is what makes the compiler check that claim.