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.”
What is the main danger of making fields public?
Access Modifiers: The Full Picture
Java has four access levels. You have been using public and private. Here is the complete table:
| Modifier | Same Class | Same Package | Subclass | Everywhere |
|---|---|---|---|---|
public |
Yes | Yes | Yes | Yes |
protected |
Yes | Yes | Yes | No |
| (package-private) | Yes | Yes | No | No |
private |
Yes | No | No | No |
Package-private is the default when you write no modifier at all. You will rarely use it intentionally in this course.
For CSCD 210, the rule is simple:
- Fields: always
private(orprivate final). - Constructors and methods in the public interface:
public. - Helper methods used only inside the class:
private. - You will use
protectedin CSCD 211 when you learn about inheritance.
The Trick: When in doubt, start with
private. You can always widen access later (make itpublic). But if you start withpublicand other code depends on it, you can never take that access away without breaking things. Start restrictive, loosen as needed.
A field declared with no access modifier (e.g., int count;) is accessible from where?
Immutable Fields with final
Some fields should never change after the object is created. A student ID, a bank account number, a creation timestamp — these are permanent. Java’s final keyword enforces this:
private final String accountNumber;
A final field:
- Must be assigned exactly once, typically in the constructor.
- Cannot be reassigned after that. The compiler rejects any attempt.
- Has no setter method — there is nothing to set.
public class BankAccount {
private final String accountNumber;
private final String owner;
private double balance;
public BankAccount(final String accountNumber, final String owner,
final double initialDeposit) {
this.accountNumber = accountNumber;
this.owner = owner;
this.balance = initialDeposit;
}
public String getAccountNumber() {
return accountNumber;
}
// No setAccountNumber() --- it's final
// No setOwner() --- it's final
}
When every field in a class is final, the object is immutable — it cannot change after construction. Java’s String class works this way. Immutable objects are safe to share between different parts of a program because no one can modify them.
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.
Why Encapsulation Matters: The Maintenance Argument
Suppose you wrote a Student class with a public gpa field and shipped it. Fifty files across the codebase directly assign to student.gpa. Now you discover that GPA must be between 0.0 and 4.0. To add that check, you must find and modify every single assignment in all fifty files.
With encapsulation, you add the check in one place:
public void setGpa(final double newGpa) {
if (newGpa < 0.0 || newGpa > 4.0) {
throw new IllegalArgumentException("GPA must be between 0.0 and 4.0");
}
this.gpa = newGpa;
}
Every caller already goes through setGpa(). Zero files need to change. The fix is instant and universal.
This is called loose coupling — the internal details of a class can change without affecting the code that uses it. It is one of the most important principles in software engineering, and it starts here, with private fields and validated setters.
Big Picture: Encapsulation is not just a Java rule to memorize. It is the reason large software projects (millions of lines of code, hundreds of developers) can exist at all. Without it, changing one class would break everything that touches it. With it, each class is a self-contained unit that protects its own rules.
You need to add a validation rule to a field. If the field is private with a setter method, how many files do you need to change?
IDE Support: Generating Getters and Setters
You do not need to type getters and setters by hand. Both IntelliJ and Eclipse can auto-generate them:
- Write your
privatefields. - Right-click (or use the Generate menu).
- Select “Generate Getters and Setters.”
- Choose which fields to include.
The IDE follows the naming conventions automatically: getName(), setName(), isActive(). After generation, add your validation logic to the setters. The IDE gives you the structure; you add the rules.
Quick Reference
| Concept | Code |
|---|---|
| Private field | private double balance; |
| Immutable field | private final String id; |
| Getter | public double getBalance() { return balance; } |
| Boolean getter | public boolean isActive() { return active; } |
| Setter with validation | public void setBalance(final double b) { if (b < 0) throw ...; this.balance = b; } |
| Constructor validation | if (name == null) throw new IllegalArgumentException(...); |
Summary
Public fields let any code put an object into an invalid state. Encapsulation fixes this by making fields private and exposing controlled access through public methods. Getters allow reading. Setters allow writing with validation. The final keyword locks fields permanently after construction.
The four access modifiers — public, protected, package-private, and private — control who can see what. For this course: fields are private, public interface methods are public, and helper methods are private.
The real payoff is maintenance. When rules change, you update one setter instead of hunting through fifty files. That is loose coupling, and it is why encapsulation is not optional in professional Java.
Next lesson: Every class needs three methods to work properly in collections and debugging: toString, equals, and hashCode. We will build all three.