What Are Access Modifiers?
Access modifiers are keywords in Java that determine the visibility and accessibility of classes, methods, constructors, and variables. They are the gatekeepers of your code, controlling who can see and use different parts of your program. Understanding access modifiers is fundamental to writing well-structured, secure, and maintainable Java code. They form the foundation of encapsulation—one of the four pillars of object-oriented programming.
In real-world terms, think of access modifiers like security levels in a building. A public lobby is open to everyone, a private office is restricted to one person, and a protected conference room might be accessible to employees and approved guests. Similarly, Java provides four access levels: public, protected, default (package-private), and private—each offering a different degree of visibility and restriction.
The concept of access control in programming emerged from the need to manage complexity in large software systems. As programs grew larger, developers realized they needed ways to hide implementation details and prevent unintended interactions between different parts of the codebase. Java's access modifier system was designed with these principles in mind, drawing from experiences with languages like C++ and Smalltalk. The result is a clean, hierarchical access control system that balances flexibility with protection.
Access modifiers serve multiple purposes in software design. They enable information hiding, allowing you to change internal implementations without affecting code that uses your class. They provide security, preventing sensitive data from being accessed or modified inappropriately. They support API design, clearly distinguishing what's meant for public consumption versus internal use. And they facilitate team collaboration, making it clear which parts of code are stable interfaces versus implementation details that might change.
Choosing the right access modifier is one of the most important decisions you make when designing classes. The general rule is to make things as restrictive as possible—start with private and only increase visibility when there's a genuine need. This principle of least privilege helps create robust, maintainable code where changes in one area don't unexpectedly break other parts of your application.
Real-World Analogy: Security Zones
- Private (Your Bedroom) – Only you can enter
- Default (Living Room) – Family members can access
- Protected (Front Yard) – Family and close neighbors
- Public (Street) – Anyone can walk by
- private – Visible only within the declaring class
- default (no modifier) – Visible within the same package
- protected – Visible within package + subclasses everywhere
- public – Visible everywhere
Under the Hood: How Access Control Works
Access modifiers are enforced at compile time and runtime by the JVM. Understanding how this works helps you use them effectively.
/*
* Access Modifier Visibility Diagram
* ===================================
*
* ┌─────────────────────────────────────────────────────────────────────────┐
* │ UNIVERSE (All Code) │
* │ │
* │ public members are visible here │
* │ │
* │ ┌───────────────────────────────────────────────────────────────────┐ │
* │ │ DIFFERENT PACKAGES │ │
* │ │ │ │
* │ │ ┌──────────────────────────┐ ┌──────────────────────────────┐ │ │
* │ │ │ Package: com.app.model │ │ Package: com.app.service │ │ │
* │ │ │ │ │ │ │ │
* │ │ │ class User │ │ class UserService │ │ │
* │ │ │ private password ─────┼──┼──► NOT visible │ │ │
* │ │ │ (default) helper ─────┼──┼──► NOT visible │ │ │
* │ │ │ protected data ───────┼──┼──► visible (if subclass) │ │ │
* │ │ │ public name ──────────┼──┼──► VISIBLE │ │ │
* │ │ │ │ │ │ │ │
* │ │ └──────────────────────────┘ └──────────────────────────────┘ │ │
* │ │ │ │
* │ └────────────────────────────────────────────────────────────────────┘ │
* │ │
* │ ┌────────────────────────────────────────────────────────────────────┐ │
* │ │ SAME PACKAGE │ │
* │ │ Package: com.app.model │ │
* │ │ │ │
* │ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │
* │ │ │ class User │ │ class UserValidator │ │ │
* │ │ │ │ │ │ │ │
* │ │ │ private password ──┼────┼──► NOT visible │ │ │
* │ │ │ (default) helper ──┼────┼──► VISIBLE │ │ │
* │ │ │ protected data ────┼────┼──► VISIBLE │ │ │
* │ │ │ public name ───────┼────┼──► VISIBLE │ │ │
* │ │ │ │ │ │ │ │
* │ │ │ ┌─────────────────┐ │ └─────────────────────┘ │ │
* │ │ │ │ SAME CLASS │ │ │ │
* │ │ │ │ All members │ │ │ │
* │ │ │ │ VISIBLE here │ │ │ │
* │ │ │ └─────────────────┘ │ │ │
* │ │ └─────────────────────┘ │ │
* │ │ │ │
* │ └─────────────────────────────────────────────────────────────────────┘ │
* │ │
* └──────────────────────────────────────────────────────────────────────────┘
*/
JVM Enforcement
// The compiler checks access at compile time
public class Example {
private int secret = 42;
}
// In another class:
Example e = new Example();
// e.secret = 10; // Compile ERROR: secret has private access in Example
// Even reflection respects access (by default)
Field field = Example.class.getDeclaredField("secret");
// field.get(e); // Runtime ERROR: IllegalAccessException
// Reflection CAN bypass access control (but shouldn't normally)
field.setAccessible(true); // Breaks encapsulation!
int value = (Integer) field.get(e); // Now works: 42
While reflection can bypass access control with setAccessible(true), this should be avoided in normal code. Modern Java (9+) has module system restrictions that can prevent this. Access modifiers are a design contract, not a security guarantee.
Private Access
The most restrictive access level. Private members are visible only within the declaring class—not even subclasses or other classes in the same package can access them.
public class BankAccount {
// Private fields - completely hidden from outside
private String accountNumber;
private double balance;
private String pin;
public BankAccount(String accountNumber, String pin) {
this.accountNumber = accountNumber;
this.pin = pin;
this.balance = 0.0;
}
// Public method provides controlled access
public double getBalance() {
return balance;
}
// Public method with validation
public boolean withdraw(double amount, String enteredPin) {
if (!validatePin(enteredPin)) {
return false;
}
if (amount <= 0 || amount > balance) {
return false;
}
balance -= amount;
logTransaction("WITHDRAW", amount);
return true;
}
// Private helper methods - internal implementation
private boolean validatePin(String enteredPin) {
return this.pin.equals(enteredPin);
}
private void logTransaction(String type, double amount) {
System.out.println(type + ": $" + amount);
}
}
// Usage from another class
BankAccount account = new BankAccount("123456", "1234");
// ✅ These work - public interface
double bal = account.getBalance();
boolean success = account.withdraw(100, "1234");
// ❌ These fail - private members
// account.balance = 1000000; // ERROR: balance has private access
// account.pin = "0000"; // ERROR: pin has private access
// account.validatePin("1234"); // ERROR: validatePin has private access
Private Constructors
// Singleton pattern uses private constructor
public class DatabaseConnection {
private static DatabaseConnection instance;
// Private constructor - cannot instantiate from outside
private DatabaseConnection() {
// Initialize connection
}
// Controlled access through static method
public static DatabaseConnection getInstance() {
if (instance == null) {
instance = new DatabaseConnection();
}
return instance;
}
}
// new DatabaseConnection(); // ERROR: constructor is private
DatabaseConnection db = DatabaseConnection.getInstance(); // OK
// Utility class with private constructor
public final class MathUtils {
private MathUtils() {
throw new AssertionError("Cannot instantiate utility class");
}
public static int square(int n) {
return n * n;
}
}
- Instance variables (almost always)
- Helper methods used only within the class
- Implementation details that might change
- Constructors in singleton/utility classes
- Internal state that should never be directly modified
Default (Package-Private) Access
When you don't specify any access modifier, Java uses default access. Members are visible to all classes within the same package, but invisible outside it.
package com.app.internal;
// Default access class - only visible in com.app.internal
class InternalHelper {
// Default access field
String data;
// Default access method
void processData() {
System.out.println("Processing: " + data);
}
}
// In the same package - full access
package com.app.internal;
public class MainProcessor {
public void process() {
InternalHelper helper = new InternalHelper(); // ✅ OK
helper.data = "test"; // ✅ OK
helper.processData(); // ✅ OK
}
}
// In a DIFFERENT package - NO access
package com.app.api;
import com.app.internal.InternalHelper; // ERROR: not visible
public class ApiService {
public void serve() {
// InternalHelper helper = new InternalHelper(); // ERROR!
}
}
Package-Private for API Design
/*
* Package Structure for Clean API Design
* =======================================
*
* com.myapp
* ├── api/ ← Public API package
* │ ├── UserService.java (public class)
* │ └── User.java (public class)
* │
* └── internal/ ← Internal implementation
* ├── UserRepository.java (package-private class)
* ├── UserValidator.java (package-private class)
* └── CacheManager.java (package-private class)
*
* Users of your library only see: UserService, User
* Implementation details are hidden in 'internal' package
*/
package com.myapp.api;
public class UserService {
// This is the public API
public User findUser(int id) { ... }
public void saveUser(User user) { ... }
}
package com.myapp.internal;
// Package-private - hidden from API users
class UserRepository {
User findById(int id) { ... }
void save(User user) { ... }
}
- Implementation classes not meant for public use
- Helper classes within a package
- Test classes that need package access
- Internal utilities shared within a module
Protected Access
Protected members are visible within the same package (like default) AND to subclasses in any package. This makes it ideal for inheritance scenarios where child classes need access to parent implementation.
package com.app.model;
public class Animal {
// Protected - accessible in subclasses
protected String name;
protected int age;
// Private - not accessible even in subclasses
private String internalId;
public Animal(String name, int age) {
this.name = name;
this.age = age;
this.internalId = generateId();
}
// Protected method - subclasses can override
protected void makeSound() {
System.out.println(name + " makes a sound");
}
// Protected - accessible for extension
protected void sleep() {
System.out.println(name + " is sleeping");
}
private String generateId() {
return "ANIMAL-" + System.currentTimeMillis();
}
}
// Subclass in a DIFFERENT package
package com.app.pets;
import com.app.model.Animal;
public class Dog extends Animal {
private String breed;
public Dog(String name, int age, String breed) {
super(name, age);
this.breed = breed;
}
public void displayInfo() {
// ✅ Can access protected members from parent
System.out.println("Name: " + name);
System.out.println("Age: " + age);
// ❌ Cannot access private members
// System.out.println(internalId); // ERROR!
}
@Override
protected void makeSound() {
System.out.println(name + " barks: Woof!");
}
public void napTime() {
super.sleep(); // ✅ Can call protected parent method
}
}
Protected Access Rules
/*
* Protected Access - Subtle Rules
* ================================
*
* In SAME package: Acts like default access (visible to all)
* In DIFFERENT package: Only visible through inheritance
*
* Key point: A subclass can access protected members of the parent
* through 'this' or 'super', but NOT through a different instance!
*/
package other;
public class Cat extends Animal {
public void test() {
// ✅ OK: Access through this
System.out.println(this.name);
// ✅ OK: Access through super
super.makeSound();
// ❌ NOT OK: Access through a different Animal instance!
Animal other = new Animal("Other", 1);
// System.out.println(other.name); // ERROR in different package!
}
}
- Template method pattern (methods meant to be overridden)
- Fields that subclasses legitimately need to access
- Hook methods in frameworks
- When you're designing for inheritance
Public Access
Public members are visible everywhere—any class in any package can access them. This is the least restrictive access level and should be used thoughtfully for your public API.
package com.app.api;
// Public class - accessible from anywhere
public class UserService {
// Public constant - globally accessible
public static final int MAX_USERNAME_LENGTH = 50;
// Public method - the API
public User createUser(String username, String email) {
validateInput(username, email);
User user = new User(username, email);
saveToDatabase(user);
return user;
}
// Public method - part of API
public User findByUsername(String username) {
return queryDatabase(username);
}
// Private - implementation detail
private void validateInput(String username, String email) {
// validation logic
}
private void saveToDatabase(User user) {
// database logic
}
private User queryDatabase(String username) {
// query logic
return null;
}
}
// From ANY class in ANY package
import com.app.api.UserService;
public class MyApp {
public static void main(String[] args) {
UserService service = new UserService();
// ✅ Public methods accessible
User user = service.createUser("john", "john@example.com");
int max = UserService.MAX_USERNAME_LENGTH;
// ❌ Private methods NOT accessible
// service.validateInput(...); // ERROR
}
}
// ❌ BAD: Public mutable field
public class User {
public String password; // Anyone can set to anything!
}
// ✅ GOOD: Private field with controlled access
public class User {
private String password;
public void setPassword(String password) {
if (password.length() < 8) {
throw new IllegalArgumentException("Too short");
}
this.password = hashPassword(password);
}
}
// ✅ OK: Public immutable field (constant)
public static final int MAX_SIZE = 100;
Complete Access Level Comparison
| Modifier | Same Class | Same Package | Subclass (diff pkg) | Everywhere |
|---|---|---|---|---|
private |
Yes | No | No | No |
(default) |
Yes | Yes | No | No |
protected |
Yes | Yes | Yes* | No |
public |
Yes | Yes | Yes | Yes |
*Protected access in subclasses only through inheritance (this/super), not arbitrary instances.
Access Modifiers for Different Elements
| Element | public | protected | default | private |
|---|---|---|---|---|
| Top-level class | Yes | No | Yes | No |
| Nested class | Yes | Yes | Yes | Yes |
| Constructor | Yes | Yes | Yes | Yes |
| Method | Yes | Yes | Yes | Yes |
| Field | Yes | Yes | Yes | Yes |
| Interface method | Yes* | No | No | Yes** |
*Interface methods are implicitly public. **Private interface methods added in Java 9.
Best Practices and Design Guidelines
// ✅ BEST PRACTICE: Start restrictive, widen as needed
public class WellDesignedClass {
// Private fields - always
private String name;
private int value;
private List<String> items;
// Public constants are OK
public static final int DEFAULT_VALUE = 10;
// Public constructor for API users
public WellDesignedClass(String name) {
this.name = name;
this.value = DEFAULT_VALUE;
this.items = new ArrayList<>();
}
// Public API methods
public String getName() {
return name;
}
public void addItem(String item) {
if (validateItem(item)) {
items.add(item);
}
}
// Return defensive copy for mutable collections
public List<String> getItems() {
return new ArrayList<>(items); // Defensive copy!
}
// Protected for subclass extension points
protected void onItemAdded(String item) {
// Hook for subclasses
}
// Private helper methods
private boolean validateItem(String item) {
return item != null && !item.isEmpty();
}
}
- Fields: Almost always
private - Getters/Setters:
publiconly if needed externally - Helper methods:
private - Template/hook methods:
protected - API methods:
public - Implementation classes: package-private (default)
- Constants:
public static final
Common Pitfalls
// ❌ BAD: Returning internal mutable list
public class Team {
private List<Player> players = new ArrayList<>();
public List<Player> getPlayers() {
return players; // Exposes internal state!
}
}
// Caller can modify internal state:
team.getPlayers().clear(); // Oops! Team now has no players
// ✅ GOOD: Return defensive copy or unmodifiable view
public List<Player> getPlayers() {
return Collections.unmodifiableList(players);
// OR: return new ArrayList<>(players);
}
// Problem: Default access doesn't work across packages!
package com.app.model;
class Helper { } // Default access
package com.app.service; // DIFFERENT package!
import com.app.model.Helper; // ERROR: Helper is not public
// Solution: Either make public or move to same package
// Common misconception: protected = "protected from outside"
// Reality: protected = accessible in same package + subclasses
package com.app;
public class Parent {
protected String data;
}
class NotASubclass {
void test() {
Parent p = new Parent();
p.data = "accessible"; // ✅ Works! Same package
}
}
Common Interview Questions
Answer: private (class only), default/package-private (same package), protected (same package + subclasses), and public (everywhere). Note that "default" is not a keyword—it's the absence of a modifier.
Answer: No. Top-level classes can only be public or default (package-private). Private and protected only apply to class members and nested classes. A private top-level class would be useless since nothing could access it.
Answer: Default access is limited to the same package. Protected also includes subclasses in different packages. So protected is more permissive than default, specifically for inheritance across packages.
Answer: Yes, using setAccessible(true) on the Field/Method object. However, this breaks encapsulation and may be restricted by the module system in Java 9+. It should generally be avoided in production code.
Answer: Almost always private. This is fundamental to encapsulation. Provide public getters/setters only if external access is needed, and include validation in setters. This allows you to change internal representation without affecting external code.
Answer: No. An overriding method must have the same or less restrictive access. For example, if parent method is protected, child can make it protected or public, but not private. This ensures substitutability (Liskov Substitution Principle).
Troubleshooting Guide
Error: "X has private access in Y"
// Problem: Trying to access private member from outside class
account.balance = 1000; // ERROR: balance has private access
// Solutions:
// 1. Use public getter/setter
double bal = account.getBalance();
// 2. If you own the class, add appropriate accessor
public double getBalance() { return balance; }
Error: "X is not public in Y; cannot be accessed from outside package"
// Problem: Class or member has default access, accessed from different package
import other.Helper; // ERROR: Helper is not public
// Solutions:
// 1. Make the class public (if appropriate)
public class Helper { }
// 2. Move your class to the same package
// 3. Expose through a public interface
Error: "attempting to assign weaker access privileges"
// Problem: Overriding method has more restrictive access than parent
public class Parent {
public void doSomething() { }
}
public class Child extends Parent {
private void doSomething() { } // ERROR!
}
// Solution: Use same or less restrictive access
public void doSomething() { } // OK
// OR
protected void doSomething() { } // ERROR - still more restrictive
See Also
- OOP Principles – Encapsulation in depth
- Static vs Instance – Access in static context
- Interfaces & Abstract Classes – Access in inheritance
- SOLID Principles – Design principles related to access