Course contents

Access modifiers

The four access levels and exactly who can see what, the protected rule most people get wrong, private access between nested classes, the overriding rule, and how to choose a modifier.

FreeLesson13 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: rings of trust
  2. How it works under the hood
  3. Real world: choosing the right modifier
  4. Advanced: modules add another layer
  5. How to say it in the interview

🔐 What are access modifiers?

Access modifiers decide which code is allowed to use a class, field, method or constructor. Java has four levels: private, package-private (no keyword at all), protected and public. For example, a private field balance in Account can be read only by code written inside Account, which is how the class guarantees nobody outside sets it to -500.

Every Java developer can recite the four keywords, so interviewers don't stop there. They ask the questions that show whether you understand the rules: can a subclass in another package call protected methods on any parent object? Why can a nested class read its outer class's private fields? Why does overriding a public method as protected not compile? And as a design question: what should be public in the first place?

What you’ll learn

  • The visibility matrix for public, protected, package-private and private
  • Why a subclass in another package can't use protected members through a parent-typed reference
  • How nested classes share private access, and what nestmates (Java 11) changed
  • Why an override can widen access but never reduce it
  • A practical default for choosing modifiers, and how module exports add another layer

For this lesson, we'll focus on the four levels and the visibility matrix, the rules for top-level classes, the protected subtlety across packages, private access between nested classes (nestmates, Java 11), the overriding rule, how to choose a modifier, and how Java modules add another layer on top.

Below the line (out of scope)

  • Packages, imports and the classpath: covered in Packages and imports
  • final, static and abstract (not access modifiers, though they often sit next to them): covered in Core Java & OOP
  • Reflection in depth and MethodHandles.Lookup

The 30-second answer

private is visible only inside the top-level class that declares it, including its nested classes. Package-private, the default with no keyword, adds every class in the same package. protected adds subclasses in other packages, but only through references of the subclass's own type. public is visible everywhere, and with Java 9 modules only if the package is exported. Top-level classes can only be public or package-private. An override can widen access but never reduce it. My default is the most restrictive level that works: private fields, package-private helpers, and public only for the deliberate API.

01Picture it first: rings of trust

Think of an office building. private is your locked desk drawer: only you, and the people who sit at your own desk (nested classes), can open it. Package-private is your team's room: anyone on the team walks in. protected adds trusted contractors from other teams who extend your work, under conditions. public is the lobby: open to everyone, and since Java 9 only if the building (the module) has unlocked its front door.

Fig. 1

Nested rings: private in the centre, then package-private, then protected, then public on the outside, with each ring including everything inside it

Nested rings: private in the centre, then package-private, then protected, then public on the outside, with each ring including everything inside it

The same rules as a matrix, which is what you'd draw on a whiteboard:

Fig. 2

A grid showing which callers can see a member for each modifier: public everywhere, protected everywhere except unrelated classes in other packages, package-private only in the class and its package, private only in the class

A grid showing which callers can see a member for each modifier: public everywhere, protected everywhere except unrelated classes in other packages, package-private only in the class and its package, private only in the class

Notice two things. Each level is a strict superset of the one below it, so "moving outward" always widens access. And the orange cell, protected from a subclass in another package, is the one with conditions attached.

02How it works under the hood

Try it yourself first

Before reading on: which of the five marked lines compile? Base and Child are in different packages.

package com.acme.base;

public class Base {
    private   int secret;
    int           packageOnly;      // package-private: no keyword
    protected int count;
    public    int open;

    protected Base() {}
    protected void touch() {}
}
package com.acme.app;

import com.acme.base.Base;

public class Child extends Base {
    void demo(Child other, Base someBase) {
        open++;              // 1. ✅ public
        count++;             // 2. ✅ inherited protected member, accessed through this
        other.count++;       // 3. ✅ reference type is Child (my own type)
        someBase.count++;    // 4. ❌ count has protected access in Base
        packageOnly++;       // 5. ❌ package-private, different package
        // secret++;         //    ❌ private, never visible outside Base
    }
}

Lines 1 to 3 compile; lines 4 and 5 don't. Line 4 is the one most people get wrong, so let's look at why.

The protected subtlety (JLS 6.6.2)

Outside the package, protected doesn't mean "subclasses can touch this member on any object". It means a subclass can access it as part of implementing itself: through this, super, or a reference whose static type is the subclass (or one of its subclasses). Through a plain Base reference, the object might be some other subclass, say Sibling extends Base, and Child has no business touching Sibling's internals.

The same rule explains a puzzle you may have hit already:

Object o = new Object();
o.clone();   // ❌ clone() has protected access in Object

Your class is a subclass of Object, but o has static type Object, not your class, and you're not in java.lang. So the call fails, which is why classes that support cloning override clone() as public.

protected constructors follow a related rule: from another package, a subclass can call super(...) or create an anonymous subclass with new Base() {}, but not new Base() directly.

Where candidates slip

Many candidates describe protected as "visible to subclasses only". That's wrong in the other direction too: protected members are also visible to every class in the same package, subclass or not. So protected is more open than package-private, not less.

Top-level class rules

A top-level class (one not nested inside another) can only be public or package-private. private or protected on a top-level class is a compile error: there's no enclosing class for "private" to be relative to. A source file can contain at most one public top-level class, and the file must be named after it. Nested classes can use all four levels.

Private members and nested classes (nestmates, Java 11)

private is scoped to the top-level class body, not to the individual nested class. An inner class can read its outer class's private fields, and the outer class can call an inner class's private methods:

public class Outer {
    private int secret = 42;

    class Inner {
        int peek() { return secret; }      // ✅ same top-level class body
    }

    private static class Helper {
        private static String hidden() { return "ok"; }
    }

    String callHelper() { return Helper.hidden(); }   // ✅ also allowed
}

The JVM, however, used to see Outer and Outer$Inner as two unrelated classes in two class files, and its access check would reject one reading the other's private field. Before Java 11, javac papered over this by generating synthetic bridge methods like access$000: package-private static methods that effectively widened access to the private member. They bloated class files and confused stack traces.

Since Java 11 (JEP 181, nestmates), class files record a nest: Outer lists its NestMembers, each member points to its NestHost, and the JVM itself allows private access between nestmates. No bridges are needed, and reflection follows the same rule. You can check with javap -v Outer.class | grep -A2 NestMembers.

Overriding cannot reduce visibility

An overriding method must be at least as accessible as the method it overrides. Otherwise substitution would break: code holding a Base reference calls a public method, and the actual object would have hidden it.

class Parent { public void run() {} protected void step() {} }

class Kid extends Parent {
    protected void run() {}   // ❌ attempting to assign weaker access privileges; was public
    public void step() {}     // ✅ widening protected → public is fine
}

Two consequences show up in interviews. Methods implementing an interface must be public, because interface methods are implicitly public. And private methods aren't inherited at all, so a method with the same signature in a subclass is a brand-new method, not an override (that's why @Override on it fails to compile).

03Real world: choosing the right modifier

Try it yourself first

Before reading on: you're designing a RateLimiter class for your team's shared library. Decide the access level for its token counter, its refill helper, its constructor and its tryAcquire() method.

Here's the path I take, and it works for almost every class:

  1. Fields: private, and final where possible. State is the thing you most need to protect. Expose behaviour, not data.
  2. Helpers: private, or package-private if a test or a collaborator in the same package needs them. Package-private is a great "internal API" level: visible to your package, invisible to library users.
  3. public only for the deliberate API. Every public member is a promise you'll have to keep across versions.
  4. protected only when you've designed for inheritance. A protected member is part of your public contract, because anyone can subclass a non-final class and call it. If you didn't design for extension, make the class final instead.
public final class RateLimiter {
    private final int capacity;
    private int tokens;                       // state: private
    private long lastRefillNanos;

    public RateLimiter(int capacity) {        // API: public
        this.capacity = capacity;
        this.tokens = capacity;
        this.lastRefillNanos = System.nanoTime();
    }

    public synchronized boolean tryAcquire() {   // API: public
        refill();
        if (tokens == 0) return false;
        tokens--;
        return true;
    }

    void refill() { /* ... */ }               // package-private: testable, not exported to users
}
In the real world

A common incident pattern: a team exposes a public List<Order> getOrders() that returns its internal ArrayList. Another service mutates the list it got back, and the original object's invariants break far away from the code that caused it. Access modifiers protect the reference, not the object behind it. Return List.copyOf(orders) or Collections.unmodifiableList(orders) to make encapsulation real.

Watch out

Access modifiers are a compile-time and link-time design tool, not a security boundary. On the classpath, reflection with setAccessible(true) can read private fields. Modules (below) tighten this, but don't treat private as a way to hide secrets from code running in the same JVM.

04Advanced: modules add another layer

Since Java 9, accessibility has two questions instead of one: is the member accessible by its modifier, and is its package exported to the caller's module?

module com.acme.ratelimit {
    exports com.acme.ratelimit.api;          // public types here are usable by other modules
    // com.acme.ratelimit.internal: public classes, but invisible outside this module
    opens com.acme.ratelimit.model to com.fasterxml.jackson.databind;   // deep reflection only
}
  • public + exported package: usable from other modules (that requires this one).
  • public + not exported: compile error from other modules, and reflection fails with IllegalAccessException or InaccessibleObjectException. This is how the JDK hides jdk.internal.* and, since Java 16 and 17, strongly encapsulates sun.* internals.
  • opens allows reflection into a package at runtime, including private members, without exporting it at compile time. That's what serialisation and DI frameworks need.

So modules give you a fifth, coarse level: "public inside my module". Code on the classpath lives in the unnamed module, sees all exported packages, and keeps the old rules.

Performance angle

Access checks cost nothing at steady state. The JVM verifies access when it resolves a field or method reference, once per call site, and the JIT then compiles direct calls. A private or final method can't be overridden, so the JIT can inline it without a type check, whereas a non-final public method needs class hierarchy analysis to prove there's only one implementation.

Modifier Top-level class Member Typical use
public ✅ ✅ deliberate API
protected ❌ ✅ hooks designed for subclasses
package-private ✅ (default) ✅ (default) internal API, test seams
private ❌ ✅ state and helpers

A few smaller rules that come up as follow-ups:

  • Interface members are implicitly public (fields are public static final). Since Java 9 interfaces can have private methods to share code between default methods.
  • Enum constructors are implicitly private; declaring one public or protected is a compile error.
  • A package-private method can't be overridden from another package. A subclass in a different package that declares the same signature gets a new, unrelated method, and calls through the parent type still go to the parent's version.

05How to say it in the interview

When I ask about access modifiers, I'm listening for the precise rules, not just the keywords: that protected includes the package, that cross-package protected access goes through the subclass's own type, that private is scoped to the top-level class, and that overrides can't narrow access. Then I want to hear a design opinion: what's your default, and why.

In the interview

"Java has four levels. Private is the top-level class body, including nested classes, which since Java 11 the JVM supports directly through nestmates. Package-private, with no keyword, adds the package. Protected adds subclasses in other packages, but only through this or a reference of the subclass's own type, so I can't touch protected members of an arbitrary Base object. Public is everywhere, and with modules only if the package is exported. Top-level classes are public or package-private, and an override can widen access but never narrow it. My default is the narrowest level that works: private fields, package-private internals, and public only for the API I'm willing to support."

On the whiteboard

             class  package  subclass(other pkg)  world
public         ✓       ✓            ✓              ✓
protected      ✓       ✓            ✓*             ✗
(default)      ✓       ✓            ✗              ✗
private        ✓       ✗            ✗              ✗
* only via this / super / ref of subclass type (JLS 6.6.2)
top-level: public | package-private only
private = whole top-level body → nestmates (Java 11, no access$000)
override: may widen, never narrow; interface impls must be public
modules: public + exported package = really public
default: private fields, pkg-private helpers, minimal public API
Practise this topicPackages & access: interview questions & scenarios
Further reading (6)