
So far, you’ve accessed object fields directly. Set a dog’s name with dog.name = "Buddy". Read a car’s mileage with car.mileage. This works but creates problems. Any code can set invalid values. A dog could end up with a negative age. A bank account could have its balance changed without a transaction record.
Encapsulation solves this by hiding an object’s internal data and controlling access through methods. The object protects its own state.
The Problem with Public Fields
Consider a BankAccount with public fields:
public class BankAccount {
public String accountNumber;
public double balance;
}
BankAccount account = new BankAccount();
account.balance = 1000;
// Anyone can do this:
account.balance = -500; // Invalid! But nothing stops it
account.balance = 999999999; // Fraud? No protection
Public fields let any code change the object however it wants. There’s no validation, no logging, no control. The object can’t enforce its own rules.
Access Modifiers
Java provides four access levels to control visibility:
public: Accessible from anywhere.
private: Accessible only within the same class.
protected: Accessible within the same class, subclasses, and same package.
Default (no modifier): Accessible within the same package only.
For encapsulation, the key ones are public (for the interface) and private (for the implementation).
Private Fields, Public Methods
The encapsulation pattern: make fields private, provide public methods to access them.
public class BankAccount {
private String accountNumber;
private double balance;
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
public double getBalance() {
return balance;
}
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
}
}
}
Now outside code can’t touch balance directly:
BankAccount account = new BankAccount("12345", 1000);
// account.balance = -500; // Error! balance is private
account.deposit(200); // Works - goes through the method
account.withdraw(50); // Works - validated
System.out.println(account.getBalance()); // 1150
The object controls its own data. Deposits must be positive. Withdrawals can’t exceed the balance. The rules are enforced.
Getters and Setters
Methods that read private fields are called getters. Methods that modify them are called setters. By convention, they’re named get[Field] and set[Field].
public class Person {
private String name;
private int age;
// Getter for name
public String getName() {
return name;
}
// Setter for name
public void setName(String name) {
this.name = name;
}
// Getter for age
public int getAge() {
return age;
}
// Setter for age with validation
public void setAge(int age) {
if (age >= 0 && age <= 150) {
this.age = age;
}
}
}
Getters typically just return the value. Setters can include validation.
Person person = new Person();
person.setName("Alice");
person.setAge(25);
person.setAge(-5); // Ignored - validation rejects it
System.out.println(person.getName()); // Alice
System.out.println(person.getAge()); // 25
Boolean Getters
For boolean fields, the getter convention uses “is” instead of “get”:
public class Task {
private boolean completed;
public boolean isCompleted() { // Not getCompleted()
return completed;
}
public void setCompleted(boolean completed) {
this.completed = completed;
}
}
Read-Only Fields
Provide a getter without a setter to make a field read-only from outside:
public class Employee {
private String employeeId;
private String name;
public Employee(String employeeId, String name) {
this.employeeId = employeeId;
this.name = name;
}
// Getter only - no setter
public String getEmployeeId() {
return employeeId;
}
// Both getter and setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
The employee ID is set once in the constructor and can never be changed. The name can be updated.
Employee emp = new Employee("E001", "Alice");
System.out.println(emp.getEmployeeId()); // E001
emp.setName("Alice Smith"); // Works
// emp.setEmployeeId("E999"); // No such method - can't change ID
Computed Properties
Getters don’t have to return stored fields. They can compute values:
public class Rectangle {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
// Computed - no "area" field exists
public double getArea() {
return width * height;
}
// Computed - no "perimeter" field exists
public double getPerimeter() {
return 2 * (width + height);
}
}
From outside, getArea() looks like any other getter. The caller doesn’t know or care whether area is stored or calculated. This flexibility is a key benefit of encapsulation.
Protecting Mutable Objects
Be careful with getters that return mutable objects like arrays or ArrayLists:
public class Team {
private ArrayList<String> members;
public Team() {
members = new ArrayList<>();
}
// Dangerous! Returns the actual list
public ArrayList<String> getMembers() {
return members;
}
}
Team team = new Team();
ArrayList<String> list = team.getMembers();
list.add("Intruder"); // Modifies the team's internal list!
The getter returns a reference to the internal list. Outside code can modify it directly, bypassing any controls.
Solutions:
// Option 1: Return a copy
public ArrayList<String> getMembers() {
return new ArrayList<>(members);
}
// Option 2: Return an unmodifiable view
public List<String> getMembers() {
return Collections.unmodifiableList(members);
}
// Option 3: Don't expose the list at all
public void addMember(String name) {
members.add(name);
}
public boolean hasMember(String name) {
return members.contains(name);
}
public int getMemberCount() {
return members.size();
}
Option 3 is often best. Provide specific methods for what callers actually need instead of exposing internal structures.
Why Encapsulation Matters
Validation. Setters can reject invalid values. An age can’t be negative. A price can’t be below zero.
Flexibility. You can change internal implementation without affecting code that uses the class. Store temperature in Celsius internally but provide getFahrenheit() and getCelsius() methods.
Control. Track changes, log access, enforce business rules. Every withdrawal could log a transaction.
Consistency. The object maintains its own valid state. You can’t have a Rectangle with negative dimensions if the setter prevents it.
Practical Example: User Account
import java.time.LocalDateTime;
public class UserAccount {
private String username;
private String email;
private String passwordHash;
private boolean active;
private LocalDateTime lastLogin;
private int failedLoginAttempts;
public UserAccount(String username, String email, String password) {
setUsername(username);
setEmail(email);
setPassword(password);
this.active = true;
this.failedLoginAttempts = 0;
}
// Username: read-only after creation
public String getUsername() {
return username;
}
private void setUsername(String username) {
if (username == null || username.length() < 3) {
throw new IllegalArgumentException("Username must be at least 3 characters");
}
this.username = username.toLowerCase();
}
// Email: can be changed, with validation
public String getEmail() {
return email;
}
public void setEmail(String email) {
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email address");
}
this.email = email.toLowerCase();
}
// Password: write-only (no getter for security)
public void setPassword(String password) {
if (password == null || password.length() < 8) {
throw new IllegalArgumentException("Password must be at least 8 characters");
}
this.passwordHash = hashPassword(password);
}
private String hashPassword(String password) {
// In real code, use proper hashing like BCrypt
return Integer.toHexString(password.hashCode());
}
public boolean checkPassword(String password) {
return hashPassword(password).equals(this.passwordHash);
}
// Active status
public boolean isActive() {
return active;
}
public void deactivate() {
this.active = false;
}
public void activate() {
this.active = true;
this.failedLoginAttempts = 0;
}
// Login tracking
public LocalDateTime getLastLogin() {
return lastLogin;
}
public void recordSuccessfulLogin() {
this.lastLogin = LocalDateTime.now();
this.failedLoginAttempts = 0;
}
public void recordFailedLogin() {
this.failedLoginAttempts++;
if (this.failedLoginAttempts >= 5) {
deactivate();
}
}
public int getFailedLoginAttempts() {
return failedLoginAttempts;
}
}
Using the class:
UserAccount user = new UserAccount("alice", "alice@email.com", "securepass123");
System.out.println("Username: " + user.getUsername()); // alice
System.out.println("Active: " + user.isActive()); // true
// Test password
System.out.println(user.checkPassword("wrongpass")); // false
System.out.println(user.checkPassword("securepass123")); // true
// Simulate failed logins
for (int i = 0; i < 5; i++) {
user.recordFailedLogin();
}
System.out.println("Active after 5 failures: " + user.isActive()); // false
Notice the design choices:
- Username is read-only after creation
- Password has no getter (write-only for security)
- Account auto-deactivates after 5 failed logins
- Email is validated and normalized to lowercase
Common Mistakes
Getters and setters for everything. Don’t automatically generate both for every field. Think about what should be readable, writable, or neither.
Setters with no validation. If a setter just assigns the value without checking anything, you’re not getting the benefit of encapsulation.
Returning mutable internal objects. Return copies or unmodifiable views, or don’t expose internals at all.
Making everything public “for convenience.” Convenience now becomes maintenance headaches later. Take time to design proper access.
What’s Next
You’ve now covered the core OOP concepts: classes, objects, constructors, inheritance, polymorphism, and encapsulation. The next phase covers intermediate topics starting with interfaces, which define contracts that classes must fulfill without specifying implementation.
Related: Java Polymorphism | Java Interfaces
Sources
- Oracle. “Controlling Access to Members of a Class.” The Java Tutorials. docs.oracle.com
- Bloch, Joshua. “Effective Java.” 3rd Edition. Addison-Wesley, 2018.


