Encapsulation, one of the four fundamental OOP concepts, is the mechanism of wrapping the data (variables) and code acting on the data (methods) together as a single unit.
The main purpose of encapsulation is to ensure data hiding. To achieve this, you must:
private.public "setter" and "getter" methods to modify and view the variables' values.To access the private variables, we use public "get" and "set" methods.
By convention, getters start with get and setters start with set, followed by the variable name (e.g., getName(), setName()).
public class Person {
private String name; // private = restricted access
// Getter
public String getName() {
return name;
}
// Setter
public void setName(String newName) {
// We can add validation logic here
if (newName != null && !newName.isEmpty()) {
this.name = newName;
}
}
}
public class Main {
public static void main(String[] args) {
Person myObj = new Person();
// myObj.name = "John"; // This would cause an error
myObj.setName("John"); // Set the value of the name variable to "John"
System.out.println(myObj.getName());
}
}
In the setName method, we added a simple validation check. This is a key benefit of encapsulation: you control how your data is set, preventing invalid data from being assigned to your object's attributes.
The true power of Encapsulation isn't just hiding data—it is protecting it.
🏦 The "Bank Account" Analogy: Think of your bank account. You do not have direct access to the bank's internal database variable that holds your balance (that would be dangerous, as you could just change it to a million dollars!). Instead, you have to interact with the bank teller or an ATM (the methods). The ATM enforces rules: it checks your PIN and ensures you have enough funds before allowing a withdrawal, protecting the internal data from invalid changes.
By making your variables private, you force other developers to use your public setters, where you can enforce business rules to prevent bad data!
public class Main {
static class Employee {
private int age;
public void setAge(int age) {
// Rule: Age cannot be negative or over 150
if (age > 0 && age < 150) {
this.age = age;
System.out.println("Age successfully set to: " + this.age);
} else {
System.out.println("Error: Invalid age provided! Age not updated.");
}
}
public int getAge() {
return this.age;
}
}
public static void main(String[] args) {
Employee emp = new Employee();
emp.setAge(-5); // Will print an error and protect the internal variable!
emp.setAge(30); // Will succeed
}
}
Encapsulation allows you to granularly control access, making a class (or a specific variable) read-only or write-only.
public class Main {
static class Student {
// Initialized once, maybe in a constructor
private String studentID = "STU-9923";
// Only a Getter is provided! No Setter!
public String getStudentID() {
return studentID;
}
}
public static void main(String[] args) {
Student myStudent = new Student();
System.out.println("Student ID: " + myStudent.getStudentID());
// myStudent.studentID = "STU-0000"; // Error: Cannot access private variable
// myStudent.setStudentID("STU-0000"); // Error: Method does not exist
}
}
While setters protect your data from bad updates, what prevents someone from creating an object with bad data right from the start using new?
You should apply the same encapsulation rules to your constructors! A good practice is to have your constructor call your setter methods, or contain the same validation logic, ensuring the object starts in a valid state.
public class Main {
static class Product {
private double price;
// Constructor uses validation
public Product(double initialPrice) {
if (initialPrice > 0) {
this.price = initialPrice;
} else {
System.out.println("Invalid price! Defaulting to 1.0");
this.price = 1.0;
}
}
public double getPrice() { return this.price; }
}
public static void main(String[] args) {
Product badProduct = new Product(-10.50);
System.out.println("Final Price: $" + badProduct.getPrice());
}
}
There is a common, hidden pitfall in encapsulation: returning mutable objects (objects that can be changed, like an ArrayList or a Date).
If your private variable is an ArrayList, and your public getter simply returns that ArrayList, external code can add or remove items from your list without ever calling a setter! This completely breaks encapsulation.
🛡️ The Solution: Return a Defensive Copy. Instead of handing out the original object, hand out a clone of the object. If external code modifies the clone, your internal data remains safe!
import java.util.ArrayList;
import java.util.List;
public class Main {
static class SecureVault {
private List<String> secretCodes = new ArrayList<>();
public SecureVault() {
secretCodes.add("ALPHA-123");
}
// BAD: Returns the original list. Anyone can modify it!
// public List<String> getSecretCodes() { return secretCodes; }
// GOOD: Returns a new copy of the list.
public List<String> getSecretCodes() {
return new ArrayList<>(secretCodes);
}
}
public static void main(String[] args) {
SecureVault vault = new SecureVault();
List<String> stolenCodes = vault.getSecretCodes();
// The thief tries to add a fake code to the vault
stolenCodes.add("FAKE-CODE");
// But the vault is safe because they only modified their copy!
System.out.println("Vault contains FAKE-CODE? " + vault.getSecretCodes().contains("FAKE-CODE"));
}
}
withdraw(amount) is usually better than setBalance(newBalance) because it preserves business rules.What are the two main things needed to achieve encapsulation?