Encapsulation: private, Getters, and Setters
You can't reach into an ATM and grab cash. You use the screen and the keypad, and the machine checks your PIN and your balance first.
At the end of Constructors, there was a hole in the plan. The constructor carefully checked every student's grade level. But nothing stopped any other code from doing this, a moment later:
maria.gradeLevel = 99;
maria.name = null;All that checking, skipped with one line. As programs grow, and more people work on the same code, this kind of accident becomes very likely.
Encapsulation closes the hole. You lock an object's fields away so outside code can't touch them directly, and you provide a few well-guarded doors, methods, for reading and changing them.
An ATM
A bank's cash is inside the machine, behind steel. You can't reach in and take it. Instead, the ATM gives you a small set of controls: check your balance, withdraw, deposit. Every request goes through the machine, which checks your PIN and makes sure you have enough money before anything happens.
That's encapsulation:
- The data (the cash, the balance) is hidden inside.
- The methods (withdraw, deposit, check balance) are the only way in.
- The methods enforce the rules, so the data can never end up in a bad state, like a negative balance.
private: locking the fields
Put the word private in front of a field, and only code inside the same class can use it:
class BankAccount {
private String owner;
private double balance;
}Now try to touch it from Main:
BankAccount account = new BankAccount();
account.balance = 1000000;
// error: balance has private access in BankAccountThe compiler refuses. The only code that can read or change balance now lives inside BankAccount itself.
Words like private and public are called access modifiers. They control who can see a field, method, or class:
| Modifier | Who can use it |
|---|---|
private | only code inside the same class |
| (none) | code in the same package (for now: your whole program) |
public | any code, anywhere |
There's a fourth, protected, which comes up with Inheritance. The rule of thumb for fields is simple: make them private. Almost always.
Getters: letting others read
Once fields are private, outside code can't even read them. If other code needs to know a value, add a getter: a small public method that returns it.
class BankAccount {
private String owner;
private double balance;
public BankAccount(String owner) {
this.owner = owner;
this.balance = 0;
}
public String getOwner() {
return owner;
}
public double getBalance() {
return balance;
}
}By convention, a getter is named get plus the field name with a capital letter: getOwner, getBalance. For a boolean field, it's usually is instead: isActive(), isEnrolled().
Setters: letting others change, with rules
To allow changes, add a setter, a public method that takes the new value, checks it, and only then stores it:
public void setOwner(String owner) {
if (owner == null || owner.isBlank()) {
System.out.println("Owner name can't be empty.");
return;
}
this.owner = owner;
}isBlank() is a String method that answers true when the text is empty or only spaces.
The check is the whole point. Code outside the class can still change the owner, but only to a valid name. There's no way around the rule anymore.
Not every field needs a setter
Here's where encapsulation really shines. Look at the balance. Should outside code be able to call setBalance(1000000)? Of course not. Real bank accounts don't work that way. Money comes in through deposits and goes out through withdrawals, each with its own rules.
So instead of a setter, give the class methods that match what actually happens in the real world:
public void deposit(double amount) {
if (amount <= 0) {
System.out.println("Deposit must be more than zero.");
return;
}
balance += amount;
}
public boolean withdraw(double amount) {
if (amount <= 0 || amount > balance) {
return false;
}
balance -= amount;
return true;
}Now it's impossible for the balance to go negative, no matter what the rest of the program does. withdraw returns true or false so the caller knows whether it worked.
Some fields shouldn't change at all after the object is created, like a student ID number. Give those a getter and no setter. You can even mark the field final, like the constants in Variables and Constants, and Java will make sure it's set once, in the constructor, and never again:
private final String studentId;Why go to all this trouble?
It can feel like extra typing: a private field, then a getter, then a setter, for every piece of data. Here's what you get for it:
- Your objects are always valid. Every change goes through your checks, so an account can never have a negative balance, and a student can never be in grade 99.
- Bugs are easier to find. If a value is wrong, only the few methods inside the class could have changed it. You know exactly where to look.
- You can change the inside without breaking the outside. Maybe later you'll store the balance in cents as a
longinstead of adouble. As long asgetBalance()still returns the right number, no other code needs to change.
That last one matters more as programs grow. The public methods are a promise to the rest of the program. What's behind them is your business.
Your editor can help
Writing getters and setters by hand gets repetitive. Most Java editors can generate them for you: in IntelliJ IDEA, it's under Code, then Generate; in VS Code with the Java extensions, right-click in the class and choose Source Action. Generated setters don't include any checks, though. Add those yourself, for any field that has rules.
Try it
This program builds a bank account with private fields, a getter for each, a guarded setter, and deposit and withdraw methods instead of a balance setter. Some of the requests in main break the rules. Predict what happens to each one.
Running Java here
Running Java right in the browser is coming to Syntaxia soon. On your own computer, save this program as Main.java and run it with java Main.java, or use your editor's Run button.
Check your prediction
Deposit must be more than zero.
Balance: 500.0
Withdraw 200: done
Withdraw 1000: refused
Owner name can't be empty.
Maria S. Santos has 300.0 pesos.The negative deposit and the blank name were both rejected by the class's own checks. The 1000 withdrawal was refused because only 300 was left. And there's no way for main to skip those checks: try adding account.balance = 1000000; and the compiler stops you.
Try it yourself
- Add the line
account.balance = 1000000;tomain. Read the compiler's error message. Then remove it. - Add a
private int withdrawalCountfield that goes up by one on every successful withdrawal, and a getter for it. Should it have a setter? Why or why not? - Add a rule to
withdraw: no single withdrawal can be more than 5,000 pesos, even if the balance is higher. Notice that no code inmainneeds to change.
Check your understanding
Up next
A school has students, but it also has teachers, and both are people with a name and an ID. Writing those shared parts twice would be a waste. In Inheritance, you'll build one class on top of another and share the common parts.
