β˜• Java

Java Access Modifiers β€” public, private, protected & default

Everything you need to know about Java Access Modifiers β€” visibility rules, encapsulation, class-level vs member-level access, inheritance behaviour, interface rules, anti-patterns, and real-world production code examples.

πŸ“…

Last Updated

March 2026

⏱️

Read Time

24 min

🎯

Level

Beginner

🏷️

Chapter

18 of 35

What are Access Modifiers in Java?

Access modifiers (also called access specifiers) in Java are keywords that define the visibility and accessibility of classes, methods, constructors, and fields. They are the primary mechanism Java provides to implement encapsulation β€” the OOP principle of hiding internal implementation details and exposing only what is necessary.

Java provides four access modifiers: public, private, protected, and default (package-private β€” applied when no modifier keyword is written). Each defines a different level of visibility, ranging from completely open (public) to completely restricted (private).

In real-world applications, access control is everywhere: a BankAccount class keeps its balance field private so only its own methods can modify it. A paymentGateway method is public so external systems can call it. An internal helper method like validateCVV() is private so no outside class can bypass security. This careful control is what separates robust production code from fragile code that anyone can break.

public Modifier β€” Maximum Visibility

The public modifier grants unrestricted access from anywhere in the program β€” the same class, the same package, subclasses in different packages, and completely unrelated classes in other packages. It is the most permissive modifier. public is used for class definitions that form the API β€” the intentional public contract that other code is meant to use.

πŸ“Œ
Where public Can Be Applied

β€’ Top-level classes (the class itself) β€’ Methods (instance and static) β€’ Fields/variables (though usually combined with final for constants) β€’ Constructors β€’ Interfaces and their members β€’ Enum declarations β€’ Nested/inner classes

πŸ“‹
Visibility Scope

Accessible from: βœ… Same class βœ… Same package βœ… Subclass in same package βœ… Subclass in different package βœ… Non-subclass in different package β†’ No restrictions β€” visible from everywhere in the program.

⚠️
One public Class Per File Rule

A .java file can contain ONLY ONE public top-level class, and the filename MUST match that class name exactly (case-sensitive). Example: PublicDemo.java must contain 'public class PublicDemo'. Violating this is a compile-time error. A file can have multiple non-public (default) classes, but only one public class.

β˜• JavaPublicModifierDemo.java
// File: PublicModifierDemo.java
// public class β€” accessible from anywhere
public class PublicModifierDemo {

    // public field β€” accessible from anywhere (avoid for mutable state)
    public static final double TAX_RATE = 0.18; // βœ… OK β€” constant

    // public mutable field β€” BAD PRACTICE (exposed to modification)
    public String name; // ❌ Avoid β€” use private + getter/setter

    // public constructor β€” anyone can instantiate
    public PublicModifierDemo(String name) {
        this.name = name;
    }

    // public method β€” part of the API contract
    public double calculateTax(double amount) {
        return amount * TAX_RATE;
    }

    // public static utility method
    public static String formatCurrency(double amount) {
        return String.format("β‚Ή%.2f", amount);
    }
}

// File: AnotherPackage/OtherClass.java (different package)
import com.techsustainify.PublicModifierDemo;

public class OtherClass {
    public void demo() {
        // βœ… Can access public class from different package
        PublicModifierDemo obj = new PublicModifierDemo("Test");

        // βœ… Can access public method
        double tax = obj.calculateTax(1000);

        // βœ… Can access public static method
        String formatted = PublicModifierDemo.formatCurrency(1180);

        // βœ… Can access public constant
        double rate = PublicModifierDemo.TAX_RATE;

        System.out.println(formatted + " (Tax: " + tax + ")");
    }
}

private Modifier β€” Maximum Restriction

The private modifier is the most restrictive access level. A private member is accessible only within the class where it is declared β€” not from subclasses, not from other classes in the same package, not from anywhere else. private is the correct default for all class fields and implementation-detail methods, forming the foundation of encapsulation.

πŸ“Œ
Where private Can Be Applied

β€’ Fields/variables (most common use) β€’ Methods (helper/utility methods) β€’ Constructors (Singleton pattern, factory pattern) β€’ Nested/inner classes β€’ ❌ Cannot be applied to top-level classes

πŸ“‹
Visibility Scope

Accessible from: βœ… Same class only ❌ Same package (other classes) ❌ Subclass in same package ❌ Subclass in different package ❌ Other packages β†’ Total isolation within the declaring class.

πŸ”’
Why private is the Right Default

Start everything as private. Only promote visibility (to protected or public) when external access is genuinely required. This is the 'Principle of Least Privilege' β€” give the minimum access needed. It enables: safe refactoring (internals can change freely), validation before state changes (via setters), immutability (no setter = immutable field), and better encapsulation.

β˜• JavaBankAccount.java β€” private in Action
public class BankAccount {

    // βœ… private fields β€” no outside code can directly modify balance
    private String accountNumber;
    private String ownerName;
    private double balance;
    private boolean frozen;

    // βœ… public constructor β€” outsiders can create accounts
    public BankAccount(String accountNumber, String ownerName, double initialDeposit) {
        this.accountNumber = accountNumber;
        this.ownerName     = ownerName;
        // Validation in constructor β€” impossible with public fields
        if (initialDeposit < 0) {
            throw new IllegalArgumentException("Initial deposit cannot be negative");
        }
        this.balance = initialDeposit;
        this.frozen  = false;
    }

    // βœ… public API methods β€” controlled access to private state
    public void deposit(double amount) {
        validateNotFrozen();
        if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive");
        balance += amount;
        logTransaction("DEPOSIT", amount);
    }

    public void withdraw(double amount) {
        validateNotFrozen();
        if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
        if (amount > balance) throw new IllegalStateException("Insufficient funds");
        balance -= amount;
        logTransaction("WITHDRAWAL", amount);
    }

    public double getBalance() {
        return balance; // Read-only access β€” no setter for balance
    }

    public String getAccountNumber() { return accountNumber; }
    public String getOwnerName()     { return ownerName; }

    // βœ… private helper methods β€” implementation details hidden
    private void validateNotFrozen() {
        if (frozen) throw new IllegalStateException("Account is frozen");
    }

    private void logTransaction(String type, double amount) {
        System.out.printf("[LOG] %s: β‚Ή%.2f | Balance: β‚Ή%.2f%n", type, amount, balance);
    }
}

// Usage:
// BankAccount acc = new BankAccount("ACC001", "Ravi", 5000);
// acc.deposit(2000);   // βœ… Works
// acc.balance = 99999; // ❌ Compile error β€” private field
// acc.logTransaction(); // ❌ Compile error β€” private method

protected Modifier β€” Inheritance-Aware Access

The protected modifier grants access to the same class, same package, AND all subclasses β€” even subclasses in different packages. It sits between default (package-only) and public (everywhere). protected is designed specifically for inheritance scenarios β€” when a parent class wants to share implementation details with its children without exposing them to the entire world.

πŸ“Œ
Where protected Can Be Applied

β€’ Methods (intended for subclass use or override) β€’ Fields (shared internal state with subclasses) β€’ Constructors (force subclassing β€” e.g., abstract base class) β€’ Nested/inner classes β€’ ❌ Cannot be applied to top-level classes

πŸ“‹
Visibility Scope

Accessible from: βœ… Same class βœ… Same package (all classes) βœ… Subclass in same package βœ… Subclass in different package ❌ Non-subclass in different package β†’ Key: package OR subclass, but NOT arbitrary outside classes.

πŸ”—
protected vs default β€” Key Difference

Both allow same-package access. The difference: protected ALSO allows subclass access across packages. default does NOT. If you have a class in package 'com.app.base' with a default method, and a subclass in 'com.app.ui', the subclass CANNOT access that method. Make it protected to enable cross-package inheritance while still blocking arbitrary external code.

β˜• JavaProtectedModifierDemo.java
// Package: com.techsustainify.shapes
public abstract class Shape {

    // protected field β€” subclasses can access and set this
    protected String color;
    protected double borderWidth;

    // protected constructor β€” forces use of subclasses
    protected Shape(String color, double borderWidth) {
        this.color       = color;
        this.borderWidth = borderWidth;
    }

    // public abstract method β€” subclasses MUST implement
    public abstract double calculateArea();

    // protected method β€” shared logic for subclasses, hidden from outside
    protected String getBaseDescription() {
        return String.format("Color: %s, Border: %.1fpx", color, borderWidth);
    }

    // protected utility for rendering β€” subclasses may override or call
    protected void drawBorder() {
        System.out.println("Drawing border: width = " + borderWidth);
    }
}

// Package: com.techsustainify.ui  (DIFFERENT package)
import com.techsustainify.shapes.Shape;

public class Circle extends Shape {

    private double radius;

    public Circle(double radius, String color) {
        super(color, 1.0); // βœ… Can call protected parent constructor
        this.radius = radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }

    public String describe() {
        // βœ… Can access protected parent method from subclass (different package)
        return "Circle [r=" + radius + "] | " + getBaseDescription();
    }

    public void render() {
        drawBorder(); // βœ… Can call protected parent method
        // βœ… Can access protected field directly
        System.out.println("Filling circle with color: " + color);
    }
}

// In a completely unrelated class (different package, NOT a subclass):
// Shape s = ...;
// s.drawBorder();       // ❌ Compile error β€” protected, not a subclass
// s.getBaseDescription(); // ❌ Compile error β€” protected

default (Package-Private) Modifier β€” Package-Level Access

The default access modifier β€” also called package-private β€” is applied when no access modifier keyword is written. A member with default access is visible to all classes within the same package but is completely hidden from classes in other packages, including subclasses in different packages. It is the "internal to this package" visibility level.

πŸ“Œ
How to Apply Default Access

Simply write NOTHING β€” no keyword at all: β€’ class DatabaseHelper { } // default class β€’ void executeQuery() { } // default method β€’ int rowCount; // default field Do NOT write 'default' as a keyword for access β€” 'default' is only a keyword in switch and interface default methods, not as an access modifier.

πŸ“‹
Visibility Scope

Accessible from: βœ… Same class βœ… Same package (all classes) ❌ Subclass in different package ❌ Non-subclass in different package β†’ Package boundary is the visibility limit.

πŸ“¦
When to Use Default Access

Use default access for: (1) Implementation classes not meant to be in the public API β€” e.g., a Helper or Impl class. (2) Package-internal utilities shared among classes in the same module/package. (3) Classes intentionally restricted to a bounded context (Domain-Driven Design bounded contexts). Well-designed Java packages often have 1-2 public API classes and several default (internal) classes.

β˜• JavaDefaultAccessDemo.java
// Package: com.techsustainify.payment

// βœ… Default (package-private) class β€” internal implementation
// Not accessible from other packages β€” intentional
class PaymentValidator {

    // Default method β€” only classes in same package can call this
    boolean isValidAmount(double amount) {
        return amount > 0 && amount <= 1_000_000;
    }

    // Default method β€” internal package utility
    boolean isValidCurrency(String currency) {
        return currency != null && (currency.equals("INR") || currency.equals("USD"));
    }
}

// Package: com.techsustainify.payment (SAME package)
// βœ… PaymentProcessor can use PaymentValidator freely
public class PaymentProcessor {

    private PaymentValidator validator = new PaymentValidator(); // βœ… Same package

    public boolean processPayment(double amount, String currency) {
        if (!validator.isValidAmount(amount)) {    // βœ… Accessible β€” same package
            System.out.println("Invalid amount");
            return false;
        }
        if (!validator.isValidCurrency(currency)) { // βœ… Accessible β€” same package
            System.out.println("Unsupported currency");
            return false;
        }
        System.out.println("Processing " + currency + " " + amount);
        return true;
    }
}

// Package: com.techsustainify.ui (DIFFERENT package)
import com.techsustainify.payment.PaymentProcessor;
// import com.techsustainify.payment.PaymentValidator; // ❌ Cannot import β€” default class

public class CheckoutUI {
    public void checkout() {
        PaymentProcessor proc = new PaymentProcessor(); // βœ… public class
        proc.processPayment(1500.0, "INR");             // βœ… public method

        // PaymentValidator v = new PaymentValidator();   // ❌ Compile error
        // v.isValidAmount(500);                          // ❌ Compile error
    }
}

Access Modifier Comparison Table β€” At a Glance

This table summarises the complete visibility matrix for all four Java access modifiers across every possible access location.

Access Locationprivatedefaultprotectedpublic
Same classβœ… Yesβœ… Yesβœ… Yesβœ… Yes
Same package β€” subclass❌ Noβœ… Yesβœ… Yesβœ… Yes
Same package β€” non-subclass❌ Noβœ… Yesβœ… Yesβœ… Yes
Different package β€” subclass❌ No❌ Noβœ… Yesβœ… Yes
Different package β€” non-subclass❌ No❌ No❌ Noβœ… Yes
Applicable to top-level class?❌ Noβœ… Yes❌ Noβœ… Yes
Applicable to class members?βœ… Yesβœ… Yesβœ… Yesβœ… Yes
Applicable to constructors?βœ… Yesβœ… Yesβœ… Yesβœ… Yes
Supports encapsulation?βœ… Bestβœ… Good⚠️ Limited❌ No
Overridable in subclass?❌ No⚠️ Same pkgβœ… Yesβœ… Yes

Class-Level vs Member-Level Access β€” Two Distinct Contexts

Access modifiers work at two distinct levels in Java: the class level (controlling access to the class itself) and the member level (controlling access to fields, methods, and constructors inside the class). The rules are different at each level, and confusing them is a common source of errors.

πŸ›οΈ
Class-Level Access (Top-Level Classes)

Top-level classes can only be public or default (package-private). They CANNOT be private or protected β€” this is a compile-time error. A public class is accessible from any package. A default class is only accessible from within its own package. One .java file = maximum one public class (must match filename).

πŸ”§
Member-Level Access (Fields, Methods, Constructors)

Members of a class can use all four modifiers: private, default, protected, and public. Member-level access is determined first by the class-level access. Even if a method is public, if the class itself is default (package-private), outside packages cannot reach that method β€” the class is not even visible to them.

🏠
Nested Class Access

Inner classes (nested inside another class) can use all four modifiers. A private inner class is accessible only within the enclosing class. A public inner class is accessible anywhere (though the outer class must also be reachable). Static nested classes follow the same rules as regular classes.

β˜• JavaClassVsMemberAccess.java
// βœ… public top-level class β€” accessible from other packages
public class Order {

    // Member-level modifiers β€” all four are valid here
    private   int orderId;          // Only Order class
    double    totalAmount;          // Default β€” same package only
    protected String customerName;  // Package + subclasses
    public    String orderStatus;   // Everywhere (but bad practice for mutable field)

    // private constructor β€” prevents direct instantiation (factory pattern)
    private Order(int orderId) {
        this.orderId = orderId;
        this.orderStatus = "PENDING";
    }

    // public factory method β€” the intended way to create orders
    public static Order create(int orderId) {
        if (orderId <= 0) throw new IllegalArgumentException("Invalid order ID");
        return new Order(orderId);
    }

    // βœ… private nested class β€” only Order can use this
    private class OrderItem {
        String productName;
        int quantity;
        double price;
    }

    // βœ… default nested class β€” accessible in same package
    class OrderAuditLog {
        void log(String event) {
            System.out.println("Order #" + orderId + ": " + event);
        }
    }
}

// ❌ private class MyClass { }   // Compile error β€” top-level class cannot be private
// ❌ protected class MyClass { } // Compile error β€” top-level class cannot be protected

Access Modifiers with Inheritance β€” Overriding Rules

Access modifiers interact with inheritance in specific, important ways. When a subclass overrides a parent method, Java enforces a critical rule: the overriding method cannot reduce visibility β€” it can only maintain or widen the access level. This ensures that code relying on the parent's interface continues to work with any subclass (Liskov Substitution Principle).

βœ…
Widening Access β€” Allowed

An overriding method CAN increase visibility: β€’ protected β†’ public βœ… β€’ default β†’ protected βœ… β€’ default β†’ public βœ… β€’ private β†’ (cannot override, so not applicable) Widening is always safe β€” if callers could call the parent version, they can certainly call a more accessible version.

❌
Narrowing Access β€” Compile Error

An overriding method CANNOT reduce visibility: β€’ public β†’ protected ❌ Compile error β€’ public β†’ default ❌ Compile error β€’ public β†’ private ❌ Compile error β€’ protected β†’ default ❌ Compile error Narrowing would break code that expects the parent's public interface to work on any subclass.

🚫
private Methods β€” Cannot Be Overridden

Private methods are not visible to subclasses, so they CANNOT be overridden. If a subclass declares a method with the same name and signature as a parent's private method, it is a completely NEW method β€” not an override. Using @Override annotation on it causes a compile error. This is called method hiding (for static methods) or simply a new unrelated method (for instance methods).

β˜• JavaAccessWithInheritance.java
// Package: com.techsustainify.vehicle
public class Vehicle {

    private String vin;          // Not inherited β€” completely private
    protected String fuelType;   // Accessible in subclasses
    public String brand;         // Accessible everywhere

    protected void startEngine() {
        System.out.println("Vehicle engine started");
    }

    public String getInfo() {
        return "Brand: " + brand + " | Fuel: " + fuelType;
    }

    private void performInternalDiagnostic() {
        System.out.println("Internal diagnostic β€” private");
    }
}

public class ElectricCar extends Vehicle {

    private int batteryCapacity;

    public ElectricCar(String brand, int batteryCapacity) {
        this.brand           = brand;       // βœ… Inherited public field
        this.fuelType        = "Electric";  // βœ… Inherited protected field
        this.batteryCapacity = batteryCapacity;
    }

    // βœ… WIDENING access: protected β†’ public (allowed)
    @Override
    public void startEngine() {
        System.out.println("Electric motor activated silently");
    }

    // βœ… MAINTAINING access: public β†’ public (same level, always allowed)
    @Override
    public String getInfo() {
        return super.getInfo() + " | Battery: " + batteryCapacity + "kWh";
    }

    // ❌ Would be NARROWING β€” compile error:
    // @Override
    // protected String getInfo() { } // ERROR β€” cannot reduce from public to protected

    // This is a NEW method, NOT an override of private performInternalDiagnostic()
    public void performInternalDiagnostic() {
        System.out.println("EV diagnostic β€” battery, motor, regenerative braking");
        // Cannot call: super.performInternalDiagnostic(); // ❌ private β€” not visible
    }
}

Encapsulation β€” The Core Purpose of Access Modifiers

Encapsulation is one of the four pillars of object-oriented programming (alongside Inheritance, Polymorphism, and Abstraction). It means bundling data (fields) and the methods that operate on that data into a single unit (class), while restricting direct access to the data. Access modifiers are the mechanism that makes encapsulation possible in Java.

πŸ”’
Data Hiding

Make all fields private. External code cannot directly read or modify internal state. Every access goes through controlled methods. Benefit: the class can enforce invariants β€” guarantees about its own state that should always hold (e.g., age can never be negative, balance can never go below minimumBalance).

πŸšͺ
Controlled Access via Getters/Setters

Provide public getters (read access) and optionally public setters (write access) for fields that need external visibility. Setters can validate input before modifying state. Read-only fields have a getter but no setter. Write-only fields (rare) have a setter but no getter. This gives you full control over every read and write.

πŸ”„
Freedom to Change Internals

Once fields are private, you can change their internal representation at any time without breaking callers. Example: storing a phone number as String internally but exposing it as a formatted PhoneNumber object through the getter. Or switching from ArrayList to LinkedList for an internal collection. Callers never know β€” they only use the public API.

β˜• JavaEncapsulatedEmployee.java
public class Employee {

    // βœ… ALL fields private β€” enforces controlled access
    private final int    employeeId;   // Immutable β€” no setter
    private       String name;
    private       String department;
    private       double salary;
    private       int    age;

    public Employee(int employeeId, String name, String department,
                    double salary, int age) {
        if (employeeId <= 0)   throw new IllegalArgumentException("Invalid ID");
        if (salary < 0)        throw new IllegalArgumentException("Salary cannot be negative");
        if (age < 18 || age > 65) throw new IllegalArgumentException("Age out of range");
        if (name == null || name.isBlank()) throw new IllegalArgumentException("Name required");

        this.employeeId = employeeId;
        this.name       = name;
        this.department = department;
        this.salary     = salary;
        this.age        = age;
    }

    // Read-only getter (no setter β€” ID is final/immutable)
    public int getEmployeeId() { return employeeId; }

    // Standard getter and setter with validation
    public String getName() { return name; }
    public void setName(String name) {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("Name required");
        this.name = name;
    }

    public double getSalary() { return salary; }
    public void setSalary(double salary) {
        if (salary < 0) throw new IllegalArgumentException("Salary cannot be negative");
        this.salary = salary;
    }

    public int getAge() { return age; }
    public void setAge(int age) {
        if (age < 18 || age > 65) throw new IllegalArgumentException("Age out of range");
        this.age = age;
    }

    // Business method β€” uses private state without exposing it
    public double calculateBonus() {
        if (salary > 100000) return salary * 0.20;
        if (salary > 50000)  return salary * 0.15;
        return salary * 0.10;
    }

    @Override
    public String toString() {
        return String.format("Employee[id=%d, name=%s, dept=%s, salary=%.2f]",
                            employeeId, name, department, salary);
    }
}

Access Modifiers in Interfaces β€” Special Rules

Interfaces in Java have special access modifier rules that differ from classes. Understanding these is essential for writing correct Java interfaces, especially since Java 8, 9, and later versions added default, static, and private methods to interfaces.

πŸ“œ
Interface Methods

β€’ Abstract methods: implicitly public β€” no other modifier allowed (private/protected/default cause compile error in abstract context). β€’ default methods (Java 8+): must be public (explicitly or implicitly). β€’ static methods (Java 8+): must be public (default) or private (Java 9+). β€’ private methods (Java 9+): allowed β€” used as internal helpers for default/static methods. NOT inherited by implementing classes.

πŸ“¦
Interface Fields

All fields in an interface are implicitly: public, static, and final β€” i.e., they are constants. You cannot make interface fields private or protected. Writing 'int MAX = 100;' in an interface is automatically 'public static final int MAX = 100;'. Attempting to assign them in implementing classes is a compile error.

πŸ›οΈ
Interface Itself

A top-level interface can only be public or default (package-private) β€” same as classes. A public interface is accessible everywhere. A default interface is package-private. Nested interfaces inside a class can be public, protected, default, or private. When implementing an interface method, the implementing class method MUST be public β€” it cannot reduce visibility.

β˜• JavaInterfaceAccessModifiers.java
// Public interface β€” accessible everywhere
public interface PaymentGateway {

    // Implicitly: public static final
    int MAX_RETRY_COUNT = 3;             // βœ… Constant
    double TRANSACTION_FEE_PERCENT = 2.5; // βœ… Constant

    // Implicitly: public abstract β€” no modifier needed
    boolean processPayment(double amount, String currency);
    void refund(String transactionId);

    // βœ… default method (Java 8+) β€” implicitly public
    default String formatAmount(double amount, String currency) {
        return String.format("%s %.2f", currency, amount);
    }

    // βœ… static method (Java 8+) β€” implicitly public
    static boolean isSupportedCurrency(String currency) {
        return currency != null &&
               (currency.equals("INR") || currency.equals("USD") || currency.equals("EUR"));
    }

    // βœ… private method (Java 9+) β€” helper for default methods
    private void logAttempt(String operation) {
        System.out.println("[Gateway] Attempting: " + operation);
    }

    // βœ… private static method (Java 9+)
    private static String buildRequestId() {
        return "REQ-" + System.currentTimeMillis();
    }
}

// Implementing class MUST use public for all interface methods
public class UPIGateway implements PaymentGateway {

    @Override
    public boolean processPayment(double amount, String currency) { // βœ… Must be public
        System.out.println("UPI payment: " + formatAmount(amount, currency));
        return true;
    }

    @Override
    public void refund(String transactionId) {
        System.out.println("UPI refund for: " + transactionId);
    }

    // ❌ Cannot do this β€” would narrow interface's public to protected
    // @Override
    // protected boolean processPayment(...) { } // Compile error
}

Common Mistakes & Pitfalls β€” Errors That Fool Everyone

These mistakes appear consistently in Java beginner and intermediate code. Each one either causes a compile-time error or β€” worse β€” silently violates the encapsulation principle, creating fragile code that breaks during maintenance.

β˜• JavaAccessModifierMistakes.java
// ❌ MISTAKE 1: public mutable fields β€” breaks encapsulation
public class Product {
    public double price;  // ❌ Anyone can set: product.price = -999;
    public int stock;     // ❌ No validation possible
}
// βœ… Fix: private fields with validated setters
public class Product {
    private double price;
    private int stock;
    public void setPrice(double price) {
        if (price < 0) throw new IllegalArgumentException("Price cannot be negative");
        this.price = price;
    }
    public double getPrice() { return price; }
}

// ❌ MISTAKE 2: Making a class private at top level
// private class Config { }  // Compile error β€” top-level class cannot be private

// ❌ MISTAKE 3: Narrowing access in subclass override
class Animal {
    public void speak() { System.out.println("..."); }
}
class Dog extends Animal {
    // @Override
    // protected void speak() { }  // ❌ Compile error β€” narrows public to protected
    @Override
    public void speak() { System.out.println("Woof!"); } // βœ… Same or wider
}

// ❌ MISTAKE 4: Expecting protected to mean 'subclass only'
// (In same package, ALL classes β€” not just subclasses β€” can access protected members)
package com.example;
class Parent {
    protected int secret = 42;
}
class UnrelatedSamePkg {  // NOT a subclass
    void demo() {
        Parent p = new Parent();
        System.out.println(p.secret); // βœ… Works! Same package, not just subclasses
    }
}

// ❌ MISTAKE 5: Overriding private method (it is not an override)
class Base {
    private void helper() { System.out.println("Base helper"); }
    public void run() { helper(); } // Calls Base's private helper
}
class Derived extends Base {
    // This is a NEW method β€” NOT an override
    private void helper() { System.out.println("Derived helper"); }
    // run() in Base still calls Base's private helper β€” polymorphism does NOT apply
}
// new Derived().run() β†’ prints 'Base helper', NOT 'Derived helper'

// ❌ MISTAKE 6: Forgetting that interface methods implementing must be public
interface Flyable {
    void fly();
}
class Bird implements Flyable {
    // void fly() { }          // ❌ Reduces public to default β€” compile error
    public void fly() { }      // βœ… Must explicitly be public

Bad Practices & Anti-Patterns β€” What Senior Developers Reject

These access modifier anti-patterns are among the top reasons for failed code reviews in professional Java teams. Each one violates fundamental OOP principles and creates long-term maintenance problems.

🚫
God Class with All-Public Fields

A class where all fields are public is not encapsulation β€” it is a glorified struct. Any class can modify its state directly, making it impossible to enforce invariants, add validation, or refactor internals. Every field should start private. Only promote visibility when genuinely needed, and almost never to public for mutable fields.

🚫
Anemic Domain Model

A class with all-private fields but only getters and setters, with no business logic β€” just data containers. This is anti-OOP. Business logic that works with the data belongs IN the class, not scattered in service classes. An Employee should have calculateBonus(), not just getSalary()/setSalary(). Tell, don't ask.

🚫
Using protected Instead of private by Default

Making all fields protected 'in case subclasses need them' is lazy access control. protected exposes internals to the entire package AND all future subclasses. This tightly couples parent and child classes, making the parent hard to refactor. Keep fields private; provide protected methods if subclasses need specific behaviour β€” not raw field access.

🚫
Unnecessary Public API β€” Leaking Internals

Making internal helper methods public because 'it might be useful later' pollutes the public API. Once public, removing or renaming a method is a breaking change. Every public method is a contract you must maintain forever. Use private or package-private for implementation details. Add to public API only with deliberate intent.

🚫
Breaking Encapsulation via Returning Mutable References

Returning a reference to a private mutable collection or array defeats encapsulation even with a private field: 'public List<Order> getOrders() { return orders; }' β€” the caller now holds a direct reference to the internal list and can modify it. Return a defensive copy: 'return Collections.unmodifiableList(orders)' or 'return new ArrayList<>(orders)'.

🚫
Reflection to Access Private Members

Using Java Reflection to access private fields (field.setAccessible(true)) in production code is an extreme violation of encapsulation β€” it bypasses all access control. It makes code brittle (breaks when field names change), causes security issues in modular Java (Java 9+ module system blocks this), and signals a design flaw. If external code needs access, provide a proper API.

β˜• JavaAccessAntiPatterns.java
// ❌ ANTI-PATTERN: Returning mutable internal collection
public class ShoppingCart {
    private List<Item> items = new ArrayList<>();

    public List<Item> getItems() {
        return items;  // ❌ Caller gets direct reference β€” can modify!
    }
}
// Usage of broken API:
// cart.getItems().clear(); // ❌ Clears internal list β€” bypasses all control!

// βœ… FIX: Return defensive copy or unmodifiable view
public class ShoppingCart {
    private List<Item> items = new ArrayList<>();

    // Option 1: Unmodifiable view β€” throws on mutation attempt
    public List<Item> getItems() {
        return Collections.unmodifiableList(items);
    }

    // Option 2: Defensive copy β€” independent copy, mutations don't affect internal state
    public List<Item> getItemsCopy() {
        return new ArrayList<>(items);
    }

    // βœ… BEST: Provide specific methods instead of exposing the collection
    public void addItem(Item item) {
        if (item == null) throw new IllegalArgumentException("Item cannot be null");
        items.add(item);
    }

    public int getItemCount() { return items.size(); }
    public boolean hasItem(String productId) {
        return items.stream().anyMatch(i -> i.getProductId().equals(productId));
    }
}

// ❌ ANTI-PATTERN: Reflection to bypass private
// Field field = SomeClass.class.getDeclaredField("privateField");
// field.setAccessible(true); // ❌ Breaks encapsulation, brittle, security risk
// field.set(obj, newValue);
// β†’ If you need this in production code, the design has a problem β€” fix the API.

Real-World Production Code Examples β€” Access Modifiers in Context

The following examples show access modifier usage patterns found in real enterprise Java codebases β€” Spring Boot application layers with deliberate, idiomatic access control at each level.

β˜• JavaUserAccount.java β€” Domain Model with Encapsulation
package com.techsustainify.domain;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class UserAccount {

    // private β€” core identity fields, immutable after creation
    private final String userId;
    private final String email;
    private final LocalDateTime createdAt;

    // private β€” mutable but controlled state
    private String displayName;
    private boolean active;
    private boolean emailVerified;
    private int loginAttempts;
    private LocalDateTime lastLoginAt;

    // private β€” internal collection never exposed directly
    private final List<String> roles = new ArrayList<>();

    // package-private constructor β€” only factory/builder in same package creates instances
    UserAccount(String userId, String email, String displayName) {
        this.userId      = userId;
        this.email       = email;
        this.displayName = displayName;
        this.createdAt   = LocalDateTime.now();
        this.active      = true;
        this.emailVerified = false;
        this.loginAttempts = 0;
    }

    // public read-only accessors for immutable fields
    public String getUserId()          { return userId; }
    public String getEmail()           { return email; }
    public LocalDateTime getCreatedAt(){ return createdAt; }

    // public controlled mutators with business logic
    public String getDisplayName() { return displayName; }
    public void setDisplayName(String displayName) {
        if (displayName == null || displayName.isBlank())
            throw new IllegalArgumentException("Display name cannot be blank");
        if (displayName.length() > 50)
            throw new IllegalArgumentException("Display name too long");
        this.displayName = displayName;
    }

    // Business state β€” read publicly, changed only through business methods
    public boolean isActive()        { return active; }
    public boolean isEmailVerified() { return emailVerified; }

    // Business methods β€” encapsulate state transitions with rules
    public void verifyEmail() {
        this.emailVerified = true;
    }

    public void deactivate() {
        this.active = false;
    }

    public void recordSuccessfulLogin() {
        this.loginAttempts = 0;
        this.lastLoginAt = LocalDateTime.now();
    }

    public boolean recordFailedLogin() {
        this.loginAttempts++;
        if (this.loginAttempts >= 5) {
            this.active = false;
            return false; // Account locked
        }
        return true;
    }

    public void addRole(String role) {
        if (!roles.contains(role)) roles.add(role);
    }

    // βœ… Unmodifiable view β€” callers read roles, cannot modify internal list
    public List<String> getRoles() {
        return Collections.unmodifiableList(roles);
    }

    public boolean hasRole(String role) {
        return roles.contains(role);
    }

    // private helper β€” implementation detail
    private boolean isLoginLocked() {
        return loginAttempts >= 5;
    }
}
β˜• JavaAbstractRepository.java β€” protected Template Method Pattern
package com.techsustainify.data;

import java.util.List;
import java.util.Optional;

// Abstract base β€” default (package-private) access β€” internal framework class
abstract class AbstractRepository<T, ID> {

    // protected β€” subclasses must access this to perform DB operations
    protected final DatabaseConnection connection;

    protected AbstractRepository(DatabaseConnection connection) {
        this.connection = connection;
    }

    // public API β€” these are the operations callers use
    public abstract Optional<T> findById(ID id);
    public abstract List<T> findAll();
    public abstract T save(T entity);
    public abstract void deleteById(ID id);

    // protected template methods β€” shared infrastructure, overridable behaviour
    protected void beforeSave(T entity) {
        // Default: no-op. Subclasses can override to add audit timestamps.
    }

    protected void afterSave(T entity) {
        // Default: no-op. Subclasses can override to publish domain events.
    }

    // private β€” internal framework utility, hidden even from subclasses
    private void checkConnectionHealth() {
        if (!connection.isHealthy()) {
            throw new DatabaseException("Connection unhealthy");
        }
    }
}

// public concrete class β€” this is what application code sees
public class UserRepository extends AbstractRepository<UserAccount, String> {

    public UserRepository(DatabaseConnection connection) {
        super(connection); // βœ… Calls protected parent constructor
    }

    @Override
    public Optional<UserAccount> findById(String userId) {
        // βœ… Uses protected connection field from parent
        return connection.query("SELECT * FROM users WHERE id = ?", userId);
    }

    @Override
    protected void beforeSave(UserAccount user) {
        // βœ… Overrides protected template β€” adds audit timestamp
        user.setLastModified(LocalDateTime.now());
    }

    @Override
    public List<UserAccount> findAll()              { /* implementation */ return null; }
    @Override
    public UserAccount save(UserAccount entity)     { /* implementation */ return null; }
    @Override
    public void deleteById(String id)               { /* implementation */ }
}

Access Modifier Decision Flowchart β€” Which One to Use?

Use this decision flowchart when choosing the right access modifier for any class member.

β–Ά Choosing access modifierfor a field, method, or constructor
Start
πŸ” Needed ONLY inside this class?no external access ever required
YES
βœ… Use privatemost restrictive β€” correct default
πŸ” Needed in same package only?not needed by other packages
YES
βœ… Use default (no keyword)package-private β€” internal to package
πŸ” Needed in subclasses too?cross-package inheritance required
YES
βœ… Use protectedpackage + subclass access
βœ… Use publicpart of public API β€” visible everywhere

Code Execution Flow β€” from source to output

Java Access Modifier Interview Questions β€” Beginner to Advanced

These questions are consistently asked in Java fresher and experienced developer interviews, campus placements, and OCPJP/OCA certification exams.

Practice Questions β€” Test Your Access Modifier Knowledge

Attempt each question independently before reading the answer β€” active recall significantly improves retention.

1. Identify the access modifier issue: public class Circle { public double radius; public double area; public Circle(double radius) { this.radius = radius; this.area = Math.PI * radius * radius; } }

Easy

2. Will this compile? If not, why? private class Config { private String dbUrl; private int port; }

Easy

3. What is the output? package com.a; public class Parent { protected int value = 10; } package com.b; import com.a.Parent; public class Child extends Parent { public void show() { System.out.println(value); // Line A Parent p = new Parent(); System.out.println(p.value); // Line B } }

Medium

4. Fix the encapsulation violation: public class Team { public List<String> members = new ArrayList<>(); public String teamName; }

Medium

5. Is the following override valid? Why or why not? class Vehicle { public void start() { System.out.println("Starting"); } protected void stop() { System.out.println("Stopping"); } } class Truck extends Vehicle { @Override protected void start() { System.out.println("Truck starting"); } @Override public void stop() { System.out.println("Truck stopping"); } }

Medium

6. What is the output and why? class Animal { private void sound() { System.out.println("Animal sound"); } public void makeSound() { sound(); } } class Dog extends Animal { public void sound() { System.out.println("Woof"); } } public class Main { public static void main(String[] args) { Animal a = new Dog(); a.makeSound(); } }

Hard

7. Design a class using all four access modifiers appropriately for a Library system book checkout.

Hard

8. What is wrong with this interface implementation? interface Printable { void print(); } class Document implements Printable { void print() { System.out.println("Printing document"); } }

Easy

Conclusion β€” Access Modifiers: The Gatekeepers of Java OOP

Access modifiers are not just a syntactic detail β€” they are the architectural foundation of well-designed Java code. They enforce encapsulation, define public contracts, protect internal invariants, and make codebases maintainable as they grow. Every field, method, and class you write deserves a deliberate access modifier choice β€” not a default assumption.

The golden rule professionals follow: start with the most restrictive modifier and only promote when genuinely necessary. Begin every field as private. Begin every helper method as private. Promote to default when package-level sharing is needed. Promote to protected only for deliberate inheritance contracts. Promote to public only for the intentional external API. This discipline β€” called the Principle of Least Privilege β€” is what separates maintainable enterprise Java from fragile, tightly-coupled code.

ModifierKeywordScopePrimary Use Case
privateprivateSame class onlyFields, internal helpers, Singleton constructors
default(none)Same packagePackage-internal classes, utilities, impl classes
protectedprotectedSame package + subclasses (cross-package)Template methods, shared state for inheritance
publicpublicEverywherePublic API: service methods, constructors, constants

Your next step: Java Encapsulation β€” where you will see how private fields combined with public getters and setters create truly robust, maintainable domain objects, and how encapsulation ties together with immutability and the Builder pattern in modern Java applications. β˜•

Frequently Asked Questions β€” Java Access Modifiers