Access Modifiers

Access modifiers are Java's four levels of visibility control — the mechanism that decides which code is even allowed to see a class member, and the foundation every encapsulation strategy is built on.

Access modifiers control which code can see and use a class, field, constructor, or method. Java has exactly four levels: public, protected, package-private (the absence of any modifier), and private. Choosing the right one for each member is a core API design decision, not just a compiler formality.

The Four Access Levels

Each level controls visibility from a progressively wider set of callers. From most to least restrictive: private, package-private, protected, public.

All four fields exist in the same class but are reachable from different sets of calling code, purely based on their modifier.

public class Account {
    public String accountId;      // visible everywhere
    protected double interestRate; // visible in package + subclasses elsewhere
    double balance;                 // no modifier: package-private
    private String pin;            // visible only inside Account itself
}

public

public members are visible from any code, anywhere — inside the class, inside the package, in subclasses, and from completely unrelated packages. It is the widest possible access and forms the outward-facing API of a class.

public methods are the contract a class exposes to the rest of the program — anything else can rely on this method existing and behaving as documented.

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}
// any class, in any package, can call:
// new Calculator().add(2, 3)

protected

protected members are visible within the same package (like package-private), plus in subclasses in other packages — but only through inheritance, and specifically through a reference typed as the subclass or accessed via this/super inside subclass code, not through an arbitrary superclass reference.

Circle can see area because it inherits it as a subclass of Shape, even though Circle lives in a different package.

// package: shapes
package shapes;
public class Shape {
    protected double area;
}

// package: shapes.extra
package shapes.extra;
import shapes.Shape;

public class Circle extends Shape {
    void printArea() {
        System.out.println(this.area); // OK: accessed via inheritance, same class hierarchy
    }
}

Package-Private (Default Access)

Writing no modifier at all gives a member package-private access: visible to any class in the same package, and invisible to every other package — including subclasses in other packages.

Package-private is Java's way of grouping tightly cooperating classes without exposing their internals outside that group.

package inventory;

class WarehouseItem {   // package-private class
    int quantity;        // package-private field
}

// any class in package "inventory" can use WarehouseItem and quantity directly
// classes in other packages cannot see WarehouseItem at all

private

private members are visible only inside the declaring class itself — not subclasses, not other classes in the same package, nowhere else. This is the tightest level and the default choice for internal fields and helper methods.

masterKey and isStrong are implementation details; only setMasterKey, a deliberately chosen public entry point, is exposed to the outside.

public class PasswordVault {
    private String masterKey;

    private boolean isStrong(String candidate) {
        return candidate.length() >= 12;
    }

    public boolean setMasterKey(String candidate) {
        if (!isStrong(candidate)) return false;
        this.masterKey = candidate;
        return true;
    }
}

Access Rules Across Contexts

The table below summarizes who can reach a member at each access level.

Caller's relationship to the member       private   package-private   protected   public
----------------------------------------  --------  ----------------  ----------  ------
Same class                                   yes           yes             yes       yes
Same package, different class                 no           yes             yes       yes
Subclass, different package                   no            no             yes*      yes
Unrelated class, different package             no            no              no       yes

* protected in a subclass from another package: accessible only via inheritance
  (through "this"/subclass-typed references), not via an arbitrary superclass reference.

Class-Level Access Modifiers

A top-level class can only be public or package-private — private and protected are not legal on a top-level class declaration (they are legal on nested/inner classes). A public class must live in a file matching its name.

Package-private top-level classes are a common way to keep helper classes invisible outside the package while still using them freely within it.

// file: Vehicle.java
public class Vehicle { }   // OK: public top-level class

class Engine { }            // OK: package-private top-level class in the same file

// private class Wheel { }  // compile error: illegal modifier for a top-level class

Design Implications

The general guideline is to default to the most restrictive access that still works, and widen only when there is a real reason. Fields are almost always private; methods meant purely as internal helpers are private; only the minimum surface a class truly needs to expose should be public. Wider access, once published, becomes very hard to narrow later — other code will have started depending on it.

Keeping items private and exposing only addItem and a read-only view of getItems means Order fully controls how its internal list can change.

public class Order {
    private final List<String> items = new ArrayList<>();

    public void addItem(String item) {   // controlled entry point
        if (item == null || item.isBlank()) {
            throw new IllegalArgumentException("item required");
        }
        items.add(item);
    }

    public List<String> getItems() {     // consider returning a copy/unmodifiable view
        return List.copyOf(items);
    }
}

Practical Usage

protected is common in classes explicitly designed to be extended (framework base classes), where subclasses need access to internal state or hook methods that outside callers should never touch directly. Package-private is common for helper classes and utility methods used only within a single package's implementation.

Complete Example

Each field's modifier reflects exactly how widely it needs to be seen: title is part of the public identity, checkoutCount is meant for subclass use, shelfCode is an internal library detail, and reserved is nobody's business but Book's own.

package library;

public class Book {
    public final String title;          // outward-facing identity, safe to expose
    protected int checkoutCount;         // subclasses may need to adjust this
    String shelfCode;                    // package-internal detail
    private boolean reserved;            // strictly internal

    public Book(String title) {
        this.title = title;
    }

    public boolean isReserved() {
        return reserved;
    }

    void reserve() { // package-private: only library-package code manages reservations
        reserved = true;
    }
}

Mental Model

Think of the four levels as four concentric rings, from the innermost (private, only this class) outward through package-private (this package), protected (this package plus subclasses elsewhere), to public (everyone). A member is visible to a caller only if that caller falls within the ring the member was declared at.

Quick Reference

Modifier          Same class   Same package   Subclass (other package)   Everywhere
---------------   ----------   ------------   -------------------------   ----------
private              yes            no                    no                  no
(none) package        yes            yes                   no                  no
protected             yes            yes                  yes (via inherit.)   no
public                yes            yes                  yes                 yes

Key Takeaways

Java has four access levels: private, package-private (no modifier), protected, and public.

private restricts access to the declaring class only; public allows access from anywhere.

package-private allows access from any class in the same package.

protected extends package-private access to subclasses in other packages, but only through inheritance.

A top-level class may only be public or package-private.

Default to the most restrictive access that works — it is easy to widen later, hard to narrow.

public class Account { public double balance; // any code anywhere can set this directly } account.balance = -500; // no validation possible public class Account { private double balance; public void deposit(double amount) { if (amount <= 0) throw new IllegalArgumentException("amount must be positive"); balance += amount; } }

A public mutable field bypasses any validation logic entirely; keeping it private and exposing controlled methods lets the class enforce its own invariants.

// package: shapes class Circle extends Shape { void test(Shape other) { System.out.println(other.area); // compile error if "area" is protected // protected access via an unrelated Shape reference is not guaranteed } } class Circle extends Shape { void test(Circle other) { System.out.println(other.area); // OK: accessed through the subclass type } } protected access from a subclass in another package works only through the subclass's own inheritance path, not through an arbitrary reference typed as the superclass.

private class Helper { } // compile error: illegal modifier for top-level class class Helper { } // package-private top-level class is the tightest legal option private is not a legal modifier on a top-level class declaration; only public and package-private (no modifier) are allowed there.

public class Config { private static final String KEY = "secret"; } // another file assumes it can read Config.KEY directly — compile error public class Config { private static final String KEY = "secret"; public static String describeKeySource() { return "environment-provided"; } }

Marking something private and then designing other classes as if it were reachable is a design mismatch — decide the intended audience first, then pick the modifier that matches.

List Java's four access modifier levels from most to least restrictive.

private, package-private (no modifier), protected, public.

Can a subclass in a different package access a protected member of its superclass through a reference typed as the superclass?

Not reliably — protected access from a subclass in another package only applies when accessed through the subclass's own type (via this, a subclass-typed reference, or super), not through an arbitrary superclass-typed reference.

What access levels are legal on a top-level class declaration?

Only public and package-private (no modifier) — private and protected are not legal modifiers for a top-level class, though they are legal on nested classes.

Why is defaulting fields to private and exposing behavior through methods generally recommended?

It lets the class control and validate every way its state can change, and lets the internal representation be changed later without breaking code outside the class, since only the method contracts are exposed.

What is the practical difference between package-private and protected?

Both allow access from anywhere in the same package. protected additionally allows access from subclasses in other packages (via inheritance), while package-private does not extend beyond the declaring package at all.

Why is it risky to widen a member's access level (e.g. from private to public) after other code has already been written against the class?

Once other code can depend on the wider access, narrowing it back later becomes a breaking change; widening should be a deliberate decision, not something done reflexively to fix a compile error.

Interview-style: why doesn't Java allow "private" or "protected" on a top-level class?

A top-level class's visibility is inherently about whether other files/packages can even reference the class name at all; private would mean nothing could see it beyond its own file (contradicting how compilation units work), and protected implies an inheritance relationship that doesn't apply to a bare top-level declaration — so the language restricts top-level classes to public or package-private.

Write a package-private class Helper with one public method and explain who can construct and call it.

// file: Helper.java, package "util" package util; class Helper { public String greet(String name) { return "Hello, " + name; } }

Only classes within the "util" package can even see the class Helper (it is package-private), so only they can construct it with new Helper(). Once constructed, any code that holds a reference — necessarily obtained from within the package — can call greet(), since the method itself is public.

Design a Vehicle base class with a protected method startEngine() meant only for subclasses to call, and a public method drive() that calls it internally.

public class Vehicle { protected void startEngine() { System.out.println("engine started"); } public void drive() { startEngine(); System.out.println("driving"); } } class Car extends Vehicle { void warmUp() { startEngine(); // OK: Car inherits access via protected } } startEngine is protected so it is reachable inside the package and by subclasses like Car anywhere, but a completely unrelated class outside the package cannot call vehicle.startEngine() directly — only through the public drive() entry point.

Which line(s) compile, (1) or (2), and why?

Only (1) compiles. Line (1) accesses value through inheritance (as this Derived object's own inherited field), which protected allows across packages. Line (2) accesses value through a plain Base-typed reference from a subclass in a different package, which protected does not permit — that access path requires the reference to be typed as the subclass or accessed via inheritance, not an arbitrary superclass instance.

A teammate made every field in a class public "to save time." Explain in code why this is a problem and fix one field as an example.

// before public class Thermostat { public double targetTemp; // anything can set this to an invalid value } thermostat.targetTemp = -500; // no validation happens, object is now in a bad state // after public class Thermostat { private double targetTemp; public void setTargetTemp(double temp) { if (temp < -50 || temp > 50) { throw new IllegalArgumentException("temperature out of range: " + temp); } targetTemp = temp; } }

With a public field, any code anywhere can assign any value with no validation, so the object can end up in an invalid state. Making the field private and exposing a validating setter guarantees targetTemp can never be set to something out of range.

package a;
public class Base {
    protected int value = 10;
}

package b;
import a.Base;
public class Derived extends Base {
    public void show() {
        System.out.println(value);      // (1)
        Base other = new Base();
        System.out.println(other.value); // (2)
    }
}