Access Modifiers in Java
Introduction
Imagine you are running a high-security bank. Inside the bank, you have different zones with varying levels of security clearance.
- The Public Lobby is open to everyone—anyone can walk in.
- The Teller Counters are accessible only to bank employees (people within the same department).
- The Manager's Office can be accessed by the manager and perhaps some VIP clients with special authorization.
- The Main Vault is strictly restricted—only the vault manager with the exact combination can enter.
In Java, classes, fields, and methods act just like these bank zones, and Access Modifiers are the security badges. They determine which other parts of your code are allowed to "see" or "use" a particular class or its members. Proper use of access modifiers is the absolute bedrock of Encapsulation—protecting your data from accidental corruption or unauthorized access. Without access modifiers, every variable would be a public lobby, leading to chaotic, fragile, and insecure software.
Core Concept: First Principles of Access Control
Java provides four levels of access control, controlled by three explicit keywords and one implicit default state. The JVM enforces these boundaries at compile-time and runtime.
The Four Modifiers
-
private(The Vault):- Scope: Accessible only within the exact same class where it is declared.
- Use Case: Hiding internal state (variables) and helper methods that shouldn't be exposed.
-
Default / Package-Private (The Teller Counter):
- Keyword: None (you just leave it blank).
- Scope: Accessible by any class within the same package.
- Use Case: Grouping related classes that need to talk to each other intimately without exposing those details to the rest of the application.
-
protected(The Manager's Office):- Scope: Accessible within the same package AND by any subclasses (even if the subclass is in a different package).
- Use Case: Designing classes meant to be inherited, where child classes need access to parent internals, but the outside world shouldn't see them.
-
public(The Public Lobby):- Scope: Accessible from everywhere, across all packages and modules.
- Use Case: Defining the public API of your class—the methods you expect other developers to call.
Memory and Design Implications
At a low level, access modifiers don't change how objects are stored in the heap. A private variable takes up the exact same memory footprint as a public one. Access modifiers are primarily a compile-time mechanism (though the JVM verifier also checks them during class loading to prevent malicious bytecode manipulation). By making a field private, you force other developers to go through your public methods (getters/setters), allowing you to insert validation logic, logging, or synchronization blocks.
Code Examples
1. Beginner Example: The Bank Account
This example demonstrates why private is critical for data integrity.
public class BankAccount {
// 1. PRIVATE: The Vault. No outside class can change this directly.
private double balance;
// 2. PACKAGE-PRIVATE (Default): Used by the bank's internal auditing system in the same package.
String accountNumber;
public BankAccount(String accountNumber, double initialDeposit) {
this.accountNumber = accountNumber;
this.balance = initialDeposit;
}
// 3. PUBLIC: The public lobby. Anyone can deposit money.
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.println("Deposited: $" + amount);
}
}
// PUBLIC: Safely exposing the balance without allowing direct modification.
public double getBalance() {
return balance;
}
}
// In another class:
public class Main {
public static void main(String[] args) {
BankAccount myAccount = new BankAccount("12345", 100.0);
myAccount.deposit(50.0); // Allowed (public)
System.out.println(myAccount.getBalance()); // Allowed (public)
// myAccount.balance = 1000000.0; // ERROR: balance has private access
}
}
2. Intermediate Example: The Power of Protected
protected is often misunderstood. It is specifically designed for inheritance across package boundaries.
// File: com/gaming/engine/Entity.java
package com.gaming.engine;
public class Entity {
// PROTECTED: Accessible in com.gaming.engine AND by subclasses anywhere
protected int health;
protected int x, y;
public Entity(int health) {
this.health = health;
}
protected void takeDamage(int amount) {
this.health -= amount;
if (health <= 0) die();
}
private void die() {
System.out.println("Entity removed from game.");
}
}
// File: com/gaming/game/Player.java
package com.gaming.game; // DIFFERENT PACKAGE
import com.gaming.engine.Entity;
public class Player extends Entity {
private String name;
public Player(String name) {
super(100);
this.name = name;
}
public void getHit() {
System.out.println(name + " was hit!");
// We can call takeDamage because it is protected, and Player IS-A Entity.
takeDamage(10);
// We can access health directly for the same reason.
System.out.println("Remaining HP: " + health);
}
}
3. Advanced Example: Access Modifiers in Singletons and Immutability
Access modifiers are crucial for enforcing design patterns like the Singleton pattern and ensuring thread safety through immutability.
public final class DatabaseConnectionPool {
// PRIVATE static instance: The core of the Singleton pattern
private static DatabaseConnectionPool instance;
// PRIVATE final variables: Ensures immutability and thread safety
private final String url;
private final int maxConnections;
// PRIVATE Constructor: Prevents anyone else from saying 'new DatabaseConnectionPool()'
private DatabaseConnectionPool() {
this.url = "jdbc:mysql://localhost:3306/db";
this.maxConnections = 10;
}
// PUBLIC static method: The only way to get the instance
public static synchronized DatabaseConnectionPool getInstance() {
if (instance == null) {
instance = new DatabaseConnectionPool();
}
return instance;
}
public void executeQuery(String query) {
System.out.println("Executing: " + query);
}
}
Common Mistakes Beginners Make
1. Making Everything Public
The Mistake: To avoid compilation errors, beginners often declare every variable as public.
The Fix: This destroys encapsulation. Any class can modify your object's state directly, leading to untraceable bugs. Default to private for all fields, and only create public getters/setters if absolutely necessary.
2. Confusing Default and Protected
The Mistake: Thinking default (no modifier) and protected are the same, or that protected is more restrictive than default.
The Fix: Remember the hierarchy: private < default < protected < public. protected is strictly more open than default because it allows cross-package access via inheritance.
3. Trying to use private or protected on Top-Level Classes
The Mistake: Trying to declare a top-level class as private.
// WRONG
private class MyHelperClass { }
The Fix: Top-level classes can only be public or default. If you want a private class, it must be a Nested Class (an inner class inside another class).
Interview Q&A
Q1: What is the default access modifier in Java if you don't specify one? Answer: It is "package-private" (often just called "default"). This means the class, method, or field is accessible only to other classes located within the exact same package.
Q2: Can a subclass in a different package access a protected variable of its parent class using an instance of the parent class?
Answer: No. A subclass in a different package can only access inherited protected members through inheritance (i.e., accessing its own inherited state). It cannot create an instance of the parent class and access the protected variable on that instance.
(Note: This is a classic trick question in Java interviews!)
Q3: Why would you make a constructor private?
Answer: A private constructor prevents other classes from instantiating the object. This is primarily used in three scenarios:
- Implementing the Singleton design pattern.
- Creating a utility class that only contains
staticmethods (likejava.lang.Math). - Using the Builder pattern or static factory methods to control object creation.
Q4: Can we override a private method?
Answer: No. Private methods are not inherited by subclasses. Since they are not inherited, they cannot be overridden. If a subclass defines a method with the same signature, it is considered a completely new and independent method.
Q5: Can an interface have private methods?
Answer: Yes, starting from Java 9. Private methods in interfaces allow developers to extract common logic shared by multiple default methods within the interface without exposing that helper logic to implementing classes.
Practice Problems
-
The Library System:
- Create a package
com.library.core. Inside, create a classBookwith aprivatetitle, aprotectedauthor, and adefaulttracking ID. - Create a class
Librarianin the same package and try to access all three fields. Which ones compile? - Create a class
Readerincom.library.users. Try to access all three fields. Which ones compile?
- Create a package
-
The Immutable Configuration:
- Write a
AppConfigclass. - Make all its fields
private final. - Provide a
publicconstructor to initialize them, but only providepublicgetter methods, no setters. Ensure that once instantiated, the configuration cannot be altered.
- Write a
-
Singleton Implementation:
- Create a
CacheManagerclass. - Use a
privateconstructor and aprivate staticinstance variable. - Provide a
public staticmethod to retrieve the single instance.
- Create a
Summary
private: The strictest level. Accessible only within the same class. Use for almost all instance variables.default(package-private): No keyword. Accessible only within the same package. Use for cohesive classes that work together under the hood.protected: Accessible within the same package AND by subclasses in different packages. Use for base classes meant for inheritance.public: Accessible everywhere. Use for API contracts, main entry points, and constants.- Golden Rule: Always grant the least privilege necessary. Start with
privateand only open up access if there is a compelling, specific reason to do so.