Understanding Static, Access Modifiers, and Encapsulation in Java


1. Static in Java
The static keyword in Java is used to indicate that a member (variable, method, or block) belongs to the class rather than an instance of the class. This means static members are shared across all objects of the class and can be accessed without creating an instance.
Key Points about Static
Static Variables: Shared across all instances of the class. Useful for constants or counters.
Static Methods: Can be called without creating an object. Often used for utility methods.
Static Blocks: Used to initialize static variables when the class is loaded.
Limitations: Static methods cannot access non-static members directly, as they don’t require an instance.
Example: Static Variable and Method
public class Counter {
// Static variable shared across all instances
static int count = 0;
// Constructor increments the static count
Counter() {
count++;
}
// Static method to get the count
public static int getCount() {
return count;
}
public static void main(String[] args) {
Counter c1 = new Counter();
Counter c2 = new Counter();
System.out.println("Total instances: " + Counter.getCount()); // Output: Total instances: 2
}
}
Use Cases
Utility Classes: Methods like Math.sqrt() are static.
Constants: public static final variables for immutable constants.
Singletons: Managing a single instance of a class.
2. Access Modifiers in Java
Access modifiers control the visibility and accessibility of class members (fields, methods, constructors, etc.) in Java. They ensure proper encapsulation and security by restricting access to sensitive parts of the code.
Types of Access Modifiers
public: The member is accessible from everywhere.
protected: The member is accessible within the same package and also in subclasses (even in different packages).
default (package-private): If no modifier is specified, the member is accessible only within the same package.
private: The member is accessible only within the same class.
Access Modifier Table
Modifier | Class | Package | Subclass | Global |
public | Yes | Yes | Yes | Yes |
protected | Yes | Yes | Yes | No |
default | Yes | Yes | No | No |
private | Yes | No | No | No |
Example: Access Modifiers
package com.example;
public class AccessDemo {
public int publicVar = 1;
protected int protectedVar = 2;
int defaultVar = 3; // Package-private
private int privateVar = 4;
public void display() {
System.out.println("Public: " + publicVar);
System.out.println("Protected: " + protectedVar);
System.out.println("Default: " + defaultVar);
System.out.println("Private: " + privateVar);
}
}
class TestAccess {
public static void main(String[] args) {
AccessDemo demo = new AccessDemo();
demo.display();
// Accessing variables
System.out.println(demo.publicVar); // Accessible
System.out.println(demo.protectedVar); // Accessible in same package
System.out.println(demo.defaultVar); // Accessible in same package
// System.out.println(demo.privateVar); // Error: privateVar is not accessible
}
}
Best Practices
Use private for sensitive data to enforce encapsulation.
Use public for methods and fields that form the class’s public API.
Use protected for members that subclasses need to access.
Use default when access is limited to the package.
3. Encapsulation in Java
Encapsulation is one of the four pillars of Object-Oriented Programming (OOP). It involves bundling data (fields) and methods that operate on that data within a single unit (class) and restricting direct access to the data. This is achieved using private fields and public getter/setter methods.
Key Benefits of Encapsulation
Data Hiding: Protects the internal state of an object from unintended modifications.
Flexibility: Allows changes to the internal implementation without affecting external code.
Maintainability: Simplifies debugging by controlling access to data.
Steps to Achieve Encapsulation
Declare fields as private.
Provide public getter and setter methods to access and modify the fields.
Add validation logic in setters, if needed.
Example: Encapsulation
public class Employee {
// Private fields
private String name;
private int age;
private double salary;
// Public constructor
public Employee(String name, int age, double salary) {
this.name = name;
setAge(age); // Use setter for validation
this.salary = salary;
}
// Public getter for name
public String getName() {
return name;
}
// Public setter for name
public void setName(String name) {
if (name != null && !name.isEmpty()) {
this.name = name;
} else {
throw new IllegalArgumentException("Name cannot be null or empty");
}
}
// Public getter for age
public int getAge() {
return age;
}
// Public setter for age with validation
public void setAge(int age) {
if (age >= 18 && age <= 65) {
this.age = age;
} else {
throw new IllegalArgumentException("Age must be between 18 and 65");
}
}
// Public getter for salary
public double getSalary() {
return salary;
}
// Public setter for salary
public void setSalary(double salary) {
if (salary >= 0) {
this.salary = salary;
} else {
throw new IllegalArgumentException("Salary cannot be negative");
}
}
public static void main(String[] args) {
Employee emp = new Employee("Alice", 30, 50000);
System.out.println("Name: " + emp.getName());
System.out.println("Age: " + emp.getAge());
System.out.println("Salary: " + emp.getSalary());
// Update using setters
emp.setSalary(60000);
System.out.println("Updated Salary: " + emp.getSalary());
// This will throw an exception
// emp.setAge(15); // IllegalArgumentException
}
}
Output
Name: Alice
Age: 30
Salary: 50000.0
Updated Salary: 60000.0
Why Use Encapsulation?
Control: Setters allow validation before updating fields.
Security: Private fields prevent unauthorized access.
Modularity: Changes to the class’s internal logic don’t break external code.
Combining Static, Access Modifiers, and Encapsulation
Let’s create a practical example that combines all three concepts: a BankAccount class with static counters, access modifiers, and encapsulation.
Example: BankAccount Class
public class BankAccount {
// Static variable to track total accounts
private static int totalAccounts = 0;
// Private instance variables (encapsulation)
private String accountNumber;
private double balance;
private String owner;
// Public constructor
public BankAccount(String accountNumber, String owner, double initialBalance) {
this.accountNumber = accountNumber;
this.owner = owner;
setBalance(initialBalance); // Use setter for validation
totalAccounts++; // Increment static counter
}
// Public getter for accountNumber
public String getAccountNumber() {
return accountNumber;
}
// Public getter for balance
public double getBalance() {
return balance;
}
// Public setter for balance with validation
public void setBalance(double balance) {
if (balance >= 0) {
this.balance = balance;
} else {
throw new IllegalArgumentException("Balance cannot be negative");
}
}
// Public getter for owner
public String getOwner() {
return owner;
}
// Public setter for owner
public void setOwner(String owner) {
if (owner != null && !owner.isEmpty()) {
this.owner = owner;
} else {
throw new IllegalArgumentException("Owner cannot be null or empty");
}
}
// Public method to deposit money
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
} else {
throw new IllegalArgumentException("Deposit amount must be positive");
}
}
// Public method to withdraw money
public void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
} else {
throw new IllegalArgumentException("Invalid withdrawal amount");
}
}
// Static method to get total accounts
public static int getTotalAccounts() {
return totalAccounts;
}
public static void main(String[] args) {
BankAccount acc1 = new BankAccount("12345", "Bob", 1000);
BankAccount acc2 = new BankAccount("67890", "Alice", 2000);
acc1.deposit(500);
acc2.withdraw(300);
System.out.println("Account 1 Balance: " + acc1.getBalance()); // Output: 1500.0
System.out.println("Account 2 Balance: " + acc2.getBalance()); // Output: 1700.0
System.out.println("Total Accounts: " + BankAccount.getTotalAccounts()); // Output: 2
}
}
Explanation
Static: totalAccounts tracks the number of accounts created and is shared across all instances.
Access Modifiers: Private fields (accountNumber, balance, owner) ensure data hiding, while public getters/setters provide controlled access.
Encapsulation: Private fields with public methods (deposit, withdraw, getters, setters) enforce validation and protect the internal state.
Conclusion
Static: Use for class-level members that don’t depend on instance state, like counters or utility methods.
Access Modifiers: Choose the appropriate modifier (public, protected, default, private) to control access and ensure security.
Encapsulation: Protect data by making fields private and exposing them through public getters/setters with validation.
By mastering these concepts, you can write Java code that is modular, secure, and easy to maintain. The BankAccount example demonstrates how these concepts work together to create robust applications.
Subscribe to my newsletter
Read articles from Prathamesh Karatkar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
