Encapsulation and Access Control
Private fields, public methods, and information hiding
After this lesson, you will be able to:
- Explain why public fields create data integrity problems
- Declare fields as
privateand providepublicgetter and setter methods - Write setter methods that validate input before storing values
- Use the
finalkeyword to create immutable fields - Describe all four Java access modifiers and when to use each
- Design a
BankAccountclass that protects its balance from invalid operations
Your Bank Account Has No Lock
Imagine a bank account where anyone can walk up to the database and type in a new balance. No teller, no PIN, no verification. A customer could set their balance to ten million dollars. Another could set someone else’s balance to zero. The bank would collapse in hours.
That sounds absurd, but it is exactly what happens when you make fields public in a Java class. Any code in the entire program can reach in and change the data to anything it wants, valid or not.
public class BankAccount {
public String owner;
public double balance;
}
BankAccount acct = new BankAccount();
acct.owner = "Alice";
acct.balance = -999999.99; // Negative balance? Sure, why not.
acct.owner = ""; // Empty name? Java won't stop you.
The code compiles. It runs. No errors. But the object is now nonsense. A bank account with a negative balance of nearly a million dollars and no owner name is not a valid bank account. It is a bug waiting to happen.
Common Pitfall: “It compiles, so it must be correct.” Compilation checks syntax, not logic. A
BankAccountwith a negative balance is syntactically legal Java but semantically invalid. The compiler cannot protect you from bad data — only your code can.
The Fix: Private Fields, Public Methods
The solution is encapsulation: hide the fields and force all access through methods that enforce rules. Three steps:
- Make all fields
private— only code inside the class can touch them directly. - Provide
publicgetter methods for reading field values. - Provide
publicsetter methods for writing field values, with validation inside.
Step 1: Lock the Fields
public class BankAccount {
private String owner;
private double balance;
}
Now any code outside BankAccount that tries acct.balance = -999999.99 gets a compile error. The field is invisible to the outside world.
Step 2: Getters for Read Access
A getter is a method that returns the value of a private field. The naming convention is getFieldName():
public String getOwner() {
return owner;
}
public double getBalance() {
return balance;
}
For boolean fields, the convention is isFieldName() instead of getFieldName():
private boolean active;
public boolean isActive() {
return active;
}
Step 3: Setters with Validation
A setter is a method that modifies a field, but only after checking that the new value is valid:
public void setOwner(final String newOwner) {
if (newOwner == null || newOwner.trim().isEmpty()) {
throw new IllegalArgumentException("Owner name cannot be null or empty");
}
this.owner = newOwner;
}
Notice the final parameter — this is Dr. Steiner’s convention. It prevents accidentally reassigning the parameter inside the method. The parameter newOwner cannot be changed after it is received.
From CSCD 110: In Python, there is no real way to make an attribute private. The convention
_balance(underscore prefix) is just a hint — nothing stops external code from accessing it. Java’sprivatekeyword is enforced by the compiler. If you mark a fieldprivate, external code literally cannot access it. Period.
Complete Example: BankAccount
Here is a complete, well-encapsulated BankAccount class. Instead of simple setters for the balance, it uses domain-specific methods — deposit() and withdraw() — because “set the balance to X” is not how bank accounts work. You deposit and withdraw.
public class BankAccount {
private final String owner;
private double balance;
public BankAccount(final String owner, final double initialDeposit) {
if (owner == null || owner.trim().isEmpty()) {
throw new IllegalArgumentException("Owner cannot be null or empty");
}
if (initialDeposit < 0) {
throw new IllegalArgumentException("Initial deposit cannot be negative");
}
this.owner = owner;
this.balance = initialDeposit;
}
public String getOwner() {
return owner;
}
public double getBalance() {
return balance;
}
public void deposit(final double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit must be positive");
}
balance += amount;
}
public void withdraw(final double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Withdrawal must be positive");
}
if (amount > balance) {
throw new IllegalArgumentException("Insufficient funds");
}
balance -= amount;
}
@Override
public String toString() {
return String.format("BankAccount[%s, $%.2f]", owner, balance);
}
}
Usage:
BankAccount acct = new BankAccount("Alice", 500.00);
acct.deposit(200.00);
acct.withdraw(50.00);
System.out.println(acct); // BankAccount[Alice, $650.00]
// These all throw IllegalArgumentException:
// acct.deposit(-100.00); // "Deposit must be positive"
// acct.withdraw(9999.00); // "Insufficient funds"
// acct.withdraw(-50.00); // "Withdrawal must be positive"
Every rule is enforced in one place. No matter how many parts of the program use BankAccount, no one can create an invalid state. The class protects its own data.
Key Insight: Not every field needs a setter. The
ownerfield isfinal— it is set once in the constructor and never changes. Thebalancefield has nosetBalance()method because direct assignment bypasses the deposit/withdraw rules. Design your public interface around what makes sense for the object, not around “one getter and one setter per field.”
public?</p>
int count;) is accessible from where?</p>
Sanity Check: Should every field be final?
No. Use final only for fields that genuinely should not change. A student's name might change (marriage, legal name change). A bank balance changes with every transaction. Reserve final for identity fields (IDs, account numbers) and configuration values.
private with a setter method, how many files do you need to change?</p>