What is Encapsulation in Java?
Encapsulation is one of the four fundamental principles of Object-Oriented Programming (OOP). It refers to the bundling of data (attributes) and methods (behaviors) that operate on that data into a single unit called a class. It also involves restricting direct access to some of the object's components, which is known as data hiding.
The main goal of encapsulation is to protect the internal state of an object from unauthorized access and modification. This is achieved by making the instance variables private and providing public getter and setter methods to access and modify these variables.
In Java, encapsulation is implemented using:
- Access Modifiers: private, public, protected, and default
- Getter Methods: Methods to retrieve the values of private variables
- Setter Methods: Methods to modify the values of private variables
Access Modifiers in Java
Java provides four access modifiers to control the visibility and accessibility of classes, variables, methods, and constructors:
| Modifier | Class | Package | Subclass | World |
|---|---|---|---|---|
| private | Yes | No | No | No |
| default | Yes | Yes | No | No |
| protected | Yes | Yes | Yes | No |
| public | Yes | Yes | Yes | Yes |
Basic Encapsulation Example:
public class Student {
// Private instance variables (data hiding)
private String name;
private int age;
private String studentId;
// Public constructor
public Student(String name, int age, String studentId) {
this.name = name;
this.age = age;
this.studentId = studentId;
}
// Public getter methods
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getStudentId() {
return studentId;
}
// Public setter methods
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
if (age > 0 && age < 100) { // Validation
this.age = age;
} else {
System.out.println("Invalid age");
}
}
public void setStudentId(String studentId) {
this.studentId = studentId;
}
// Method to display student information
public void displayInfo() {
System.out.println("Name: " + name);
System.out.println("Age: " + age);
System.out.println("Student ID: " + studentId);
}
}
public class Main {
public static void main(String[] args) {
Student student = new Student("John Doe", 20, "STU001");
// Accessing data through getter methods
System.out.println("Student Name: " + student.getName());
System.out.println("Student Age: " + student.getAge());
// Modifying data through setter methods
student.setAge(21);
student.setName("John Smith");
// Trying invalid age
student.setAge(-5); // Will show error message
student.displayInfo();
}
}
Advanced Encapsulation Example:
public class BankAccount {
// Private instance variables
private String accountNumber;
private String accountHolder;
private double balance;
private String pin;
// Constructor
public BankAccount(String accountNumber, String accountHolder, String pin) {
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
this.pin = pin;
this.balance = 0.0;
}
// Getter methods
public String getAccountNumber() {
return accountNumber;
}
public String getAccountHolder() {
return accountHolder;
}
public double getBalance(String enteredPin) {
if (validatePin(enteredPin)) {
return balance;
} else {
System.out.println("Invalid PIN");
return -1;
}
}
// Setter methods with validation
public void setAccountHolder(String accountHolder) {
if (accountHolder != null && !accountHolder.trim().isEmpty()) {
this.accountHolder = accountHolder;
} else {
System.out.println("Invalid account holder name");
}
}
public void changePin(String oldPin, String newPin) {
if (validatePin(oldPin) && isValidPin(newPin)) {
this.pin = newPin;
System.out.println("PIN changed successfully");
} else {
System.out.println("PIN change failed");
}
}
// Business methods
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.println("Deposited: $" + amount);
System.out.println("New balance: $" + balance);
} else {
System.out.println("Invalid deposit amount");
}
}
public void withdraw(double amount, String enteredPin) {
if (validatePin(enteredPin)) {
if (amount > 0 && amount <= balance) {
balance -= amount;
System.out.println("Withdrawn: $" + amount);
System.out.println("New balance: $" + balance);
} else {
System.out.println("Invalid withdrawal amount or insufficient funds");
}
} else {
System.out.println("Invalid PIN");
}
}
// Private helper methods
private boolean validatePin(String enteredPin) {
return this.pin.equals(enteredPin);
}
private boolean isValidPin(String pin) {
return pin != null && pin.length() == 4 && pin.matches("\\d{4}");
}
}
public class BankDemo {
public static void main(String[] args) {
BankAccount account = new BankAccount("123456789", "Alice Johnson", "1234");
// Deposit money
account.deposit(1000);
// Try to check balance with wrong PIN
account.getBalance("0000"); // Invalid PIN
// Check balance with correct PIN
System.out.println("Balance: $" + account.getBalance("1234"));
// Withdraw money
account.withdraw(500, "1234");
// Change PIN
account.changePin("1234", "5678");
// Try old PIN (should fail)
account.withdraw(200, "1234"); // Invalid PIN
// Use new PIN
account.withdraw(200, "5678");
}
}
Benefits of Encapsulation:
- Data Hiding: Internal state is protected from unauthorized access
- Increased Security: Controlled access to data through methods
- Better Maintainability: Changes to internal implementation don't affect external code
- Validation: Data can be validated before being set
- Flexibility: Internal implementation can be changed without affecting users
- Modularity: Code is organized into logical units
Read-Only and Write-Only Properties:
public class Employee {
private String employeeId;
private String name;
private double salary;
private final String dateOfJoining;
public Employee(String employeeId, String name, double salary, String dateOfJoining) {
this.employeeId = employeeId;
this.name = name;
this.salary = salary;
this.dateOfJoining = dateOfJoining;
}
// Read-only property (no setter)
public String getEmployeeId() {
return employeeId;
}
// Read-write property
public String getName() {
return name;
}
public void setName(String name) {
if (name != null && !name.trim().isEmpty()) {
this.name = name;
}
}
// Write-only property (no getter for salary)
public void setSalary(double salary, String managerPin) {
if ("ADMIN123".equals(managerPin) && salary > 0) {
this.salary = salary;
System.out.println("Salary updated successfully");
} else {
System.out.println("Unauthorized access or invalid salary");
}
}
// Read-only property (final field)
public String getDateOfJoining() {
return dateOfJoining;
}
// Method to get salary (requires authentication)
public double getSalary(String pin) {
if ("USER123".equals(pin)) {
return salary;
} else {
System.out.println("Access denied");
return 0;
}
}
}
Encapsulation with Inheritance:
// Base class with encapsulated data
class Vehicle {
private String make;
private String model;
private int year;
public Vehicle(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
}
// Getters
public String getMake() { return make; }
public String getModel() { return model; }
public int getYear() { return year; }
// Setters with validation
public void setMake(String make) {
if (make != null && !make.trim().isEmpty()) {
this.make = make;
}
}
public void setModel(String model) {
if (model != null && !model.trim().isEmpty()) {
this.model = model;
}
}
public void setYear(int year) {
if (year >= 1900 && year <= 2025) {
this.year = year;
}
}
public void displayInfo() {
System.out.println(year + " " + make + " " + model);
}
}
// Subclass inheriting encapsulated behavior
class Car extends Vehicle {
private int doors;
private String fuelType;
public Car(String make, String model, int year, int doors, String fuelType) {
super(make, model, year);
this.doors = doors;
this.fuelType = fuelType;
}
// Getters and setters for new properties
public int getDoors() { return doors; }
public void setDoors(int doors) {
if (doors >= 2 && doors <= 5) {
this.doors = doors;
}
}
public String getFuelType() { return fuelType; }
public void setFuelType(String fuelType) {
if (fuelType != null && (fuelType.equals("Petrol") || fuelType.equals("Diesel") || fuelType.equals("Electric"))) {
this.fuelType = fuelType;
}
}
@Override
public void displayInfo() {
super.displayInfo();
System.out.println("Doors: " + doors);
System.out.println("Fuel Type: " + fuelType);
}
}
Best Practices for Encapsulation:
- Make instance variables private
- Provide public getter and setter methods
- Use meaningful names for getter and setter methods
- Include validation in setter methods
- Consider using read-only or write-only properties when appropriate
- Keep the class interface simple and intuitive
- Document the purpose and behavior of methods
Common Mistakes to Avoid:
- Don't make all variables public - this defeats the purpose of encapsulation
- Avoid getters and setters that don't add any value
- Don't expose internal implementation details
- Avoid tight coupling between classes
- Don't use encapsulation just for the sake of it
Encapsulation is a fundamental concept in Java that helps you create robust, maintainable, and secure applications. By properly encapsulating your data and providing controlled access through methods, you can protect your objects from unauthorized access and ensure data integrity. Practice implementing encapsulation in your Java projects to build better software.