Access Modifiers

Controlling Access to Classes, Methods, and Variables

← Back to Index

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

The Four Access Levels (Most to Least Restrictive)
  • 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
Reflection and Security

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;
    }
}
When to Use Private
  • 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) { ... }
}
When to Use Default Access
  • 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!
    }
}
When to Use Protected
  • 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
    }
}
Public Fields - Usually a Bad Idea
// ❌ 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();
    }
}
Access Modifier Checklist
  1. Fields: Almost always private
  2. Getters/Setters: public only if needed externally
  3. Helper methods: private
  4. Template/hook methods: protected
  5. API methods: public
  6. Implementation classes: package-private (default)
  7. Constants: public static final

Common Pitfalls

Pitfall 1: Exposing Mutable Objects
// ❌ 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);
}
Pitfall 2: Package Structure Mismatch
// 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
Pitfall 3: Protected Doesn't Mean What You Think
// 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

Q1: What are the four access modifiers in Java?

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.

Q2: Can a top-level class be private or protected?

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.

Q3: What's the difference between protected and default access?

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.

Q4: Can we access private members using reflection?

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.

Q5: What access modifier should you use for instance variables?

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.

Q6: Can an overriding method have more restrictive access?

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