Access Modifiers in Java
A subclass compiles one line and rejects the next. Child lives in package oop.p2 and extends Parent from oop.p1. Inside Child, the call this.protectedAccessMethod() is fine, while other.protectedAccessMethod(), where other is declared as Parent, does not compile. Same method, same subclass, different outcome. That is not a compiler bug: protected does not open a member to "subclasses in general", only through a reference of the subclass's own type.
Access modifiers in Java are the keywords public, protected and private that control where a class, field, method or constructor is visible from. There are three modifiers but four access levels: the fourth one, package-private (also called default access), is what you get when no modifier is written at all.
What access modifiers are
An access modifier is normally written first in a member declaration, before the other modifiers and the type. The language itself does not fix the order — public static and static public mean exactly the same thing to the compiler — but the conventional style is modifier first:
public int i;
private static double j;
private int myMethod(int a, char b) { /* ... */ }
int defaultField; // no modifier - package-private The four levels in one sentence each:
- public — the member is visible to any code that can see the class itself.
- protected — the member is visible inside its own package and, in addition, to subclasses in other packages.
- package-private (default access) — the member is visible only to code in the same package. This level has no keyword of its own.
- private — the member is visible only inside the body of its own top-level class, which includes that class and its nested classes.
Restricting access to members is the technical mechanism behind encapsulation: fields are declared private, and all work with them goes through public methods that can validate input and keep the object in a consistent state.
The four access levels at a glance
This is the table worth keeping in front of you: it answers "who can see this member" for all four levels at once.
| Access level | Same class | Another class, same package | Subclass, another package | Any class, another package |
|---|---|---|---|---|
public | Yes | Yes | Yes | Yes |
protected | Yes | Yes | Yes, but only through a reference of the subclass type | No |
| package-private (default) | Yes | Yes | No | No |
private | Yes | No | No | No |
The levels form a strict ordering from the most restrictive to the least: private → package-private → protected → public. Each one permits everything the previous one permitted, and then some.
Important
Since Java 9 the four access levels are no longer the whole story. The module system (JPMS) adds an outer layer: a public class is invisible outside its module unless its package is exported with an exports directive in module-info.java. So "public means visible to everyone" holds only inside a single module, or in projects that have no module-info.java at all.
public, private and package-private
The difference is easiest to see in code. Class Modifiers declares three fields with three different access levels. Inside the class all three are available, as its toString() shows:
package oop.p1;
public class Modifiers {
public int publicVar; // public access
private int privateVar; // private access
int defaultVar; // package-private, no modifier
@Override
public String toString() {
return "Modifiers{"
+ "publicVar=" + publicVar
+ ", privateVar=" + privateVar
+ ", defaultVar=" + defaultVar
+ '}';
}
} ModifiersExample1 sits in the same package oop.p1. From there the public field and the package-private field are both reachable, while touching the private field fails to compile:
package oop.p1;
public class ModifiersExample1 {
public static void main(String[] args) {
Modifiers object = new Modifiers();
object.defaultVar = 10; // OK: same package
object.publicVar = 20; // OK
// object.privateVar = 100; // Compile error: private outside Modifiers
}
} Now the same class in a different package, oop.p2. Here even the package-private field is out of reach. Note that the nesting of the names (oop.p1 and oop.p2) changes nothing: as far as access rules are concerned, Java packages are not nested inside one another.
package oop.p2;
import oop.p1.Modifiers;
public class ModifiersExample2 {
public static void main(String[] args) {
Modifiers object = new Modifiers();
// object.defaultVar = 10; // Compile error: different package
object.publicVar = 20; // OK
// object.privateVar = 100; // Compile error
}
} Easy to get wrong
Access modifiers apply only to classes and to class members. A local variable or a method parameter can never be private or public — its scope is already limited to the enclosing block. The only modifier a local variable accepts is final.
The protected modifier
protected is the least intuitive of the levels because it does two things at once: it grants access to the whole package, exactly like package-private, and on top of that opens the member to subclasses in other packages. The popular summary "protected is for subclasses" is therefore incomplete — inheritance adds access, it does not replace the package-level access.
Let Parent declare three methods with three different levels:
package oop.p1;
public class Parent {
public void publicAccessMethod() {
}
void defaultAccessMethod() {
}
protected void protectedAccessMethod() {
}
} A subclass in another package can reach the public and the protected method, but not the package-private one:
package oop.p2;
import oop.p1.Parent;
public class Child extends Parent {
public void someMethod() {
publicAccessMethod(); // OK
// defaultAccessMethod(); // Compile error: different package
protectedAccessMethod(); // OK: inherited protected method
}
} And here is the case from the opening paragraph. From a subclass in another package, a protected member may be accessed only through an expression whose type is the subclass itself (or a subtype of it). That is what the Java Language Specification, section 6.6.2.1 requires:
package oop.p2;
import oop.p1.Parent;
public class Child extends Parent {
public void compare(Parent other, Child sibling) {
this.protectedAccessMethod(); // OK: reference of type Child
super.protectedAccessMethod(); // OK
sibling.protectedAccessMethod(); // OK: type Child
// other.protectedAccessMethod(); // Compile error: type Parent
}
} The intent behind the restriction: a subclass is trusted with its own inherited part of the object, not with every object of the parent type that happens to exist in the system.
Next, a class AccessClass in package oop.p2 that does not extend Parent. Only the public method is available to it:
package oop.p2;
import oop.p1.Parent;
public class AccessClass {
public static void main(String[] args) {
Parent parent = new Parent();
parent.publicAccessMethod(); // OK
// parent.defaultAccessMethod(); // Compile error
// parent.protectedAccessMethod();// Compile error
}
} Move the very same AccessClass into oop.p1, next to Parent, and both the protected and the package-private members become accessible — with no inheritance whatsoever:
package oop.p1;
public class AccessClass {
public static void main(String[] args) {
Parent parent = new Parent();
parent.publicAccessMethod(); // OK
parent.defaultAccessMethod(); // OK: same package
parent.protectedAccessMethod(); // OK: same package
}
} Access levels for a class
A top-level class (one that is not nested) has only two of the four levels available:
- package-private — a class without a modifier is visible only to code in its own package.
- public — the class is visible everywhere, with the JPMS caveat noted above.
private and protected cannot be applied to a top-level class; the compiler rejects them. A nested class, on the other hand, accepts all four levels, because a nested class is a member of its enclosing class.
When we say class A has access to class B, we mean that A can:
- create an instance of
B; - declare a variable of type
Band extendB; - use those members of
Bthat are open to it.
If the class itself is invisible, nothing inside it is reachable — including its public members. Below is an attempt to extend the package-private class HotBeverage from another package; it does not compile, and the error appears already on the import line:
package oop.p1;
class HotBeverage {
} package oop.p2;
// import oop.p1.HotBeverage; // Compile error: the class is not public
public class Tea {
// extends HotBeverage - impossible from another package
} One more rule, about files: a public class must be the only public top-level type in its file, and the file name must match that class name. Any number of package-private classes may sit alongside it in the same file — but note that they are independent top-level classes and cannot see each other's private members:
// file Beverage.java
public class Beverage {
}
class HotBeverage {
}
class ColdBeverage {
} Modifiers when overriding and in interfaces
When you override a method, narrowing the access level is forbidden; widening is allowed. If the parent declares a method protected, the subclass may keep it protected or promote it to public, but never demote it to package-private or private:
package oop.p1;
public class Parent {
protected void hook() {
}
} package oop.p1;
public class Child extends Parent {
@Override
public void hook() { // OK: protected widened to public
}
// @Override
// void hook() {} // Error: attempting to assign weaker access privileges
} The reason is the Liskov substitution principle: code holding a variable of type Parent must be able to call hook() no matter which concrete subclass is plugged in.
A private method is not inherited and therefore cannot be overridden. A method with the same signature in a subclass is simply a new, unrelated method, and putting @Override on it is a compile error.
Interfaces play by their own rules:
- abstract methods are implicitly
public abstract, and fields are implicitlypublic static final; - writing
publicon an interface method is legal but redundant, and most style guides treat it as noise; - since Java 8 interfaces may declare
defaultandstaticmethods with a body (thedefaultkeyword here is about a default implementation, not about an access level); - since Java 9 interfaces may declare
privatemethods, used to factor shared code out ofdefaultmethods.
Worth knowing
The scope of private is the body of the enclosing top-level class, not just the class where the member is declared: a nested class sees the private fields of the outer class and vice versa. Do not confuse this with the file — two independent top-level classes in one .java file are strangers, and their private members are mutually invisible. Before Java 11 the compiler implemented nested-class access through synthetic bridge methods; since Java 11 the nestmates mechanism lets the JVM check membership of a common "nest" directly, with no generated methods.
Where developers get tripped up
- Assuming sub-packages inherit access. Package
oop.p1.internalis not "inside"oop.p1: for access rules these are two completely unrelated packages. - Treating
protectedand package-private as the same thing.protectedis strictly wider: the package plus subclasses everywhere. - Reaching a
protectedmember through a parent-typed reference from a subclass in another package — and then staring at the compile error. - Making a field
public"just for now". A public field becomes part of the class contract; removing it later breaks code you do not own. - Writing
publicon interface methods and believing that without it the method would be package-private. It would not — it ispubliceither way. - Forgetting constructors. A constructor takes any of the four levels too: a
privateconstructor prevents instantiation from outside, which is how utility classes, singletons and static factory methods work.
A practical design rule: start with the most restrictive level and relax it only when something concrete forces you to. Fields — private; helper methods — private or package-private; protected — only for what you deliberately offer subclasses to extend; public — only for the API you have thought through and are willing to support.
Frequently asked questions
Why is there no default keyword for package-private access?
Because package-private is expressed by the absence of a modifier: write int x; and the field is already visible to the whole package. The keyword default does exist in Java, but it means something else entirely — a branch in a switch and a default implementation in an interface. default int x; is a compile error.
What is the difference between protected and default (package-private)?
Both give access to every class in the same package. protected adds one thing on top: subclasses in other packages can use the member as well, though only through a reference whose type is the subclass or a subtype of it. In other words, protected is strictly wider than package-private, never narrower.
Can an overriding method reduce visibility?
No. You may keep the level or widen it: package-private can become protected or public, and protected can become public. Narrowing produces the compile error "attempting to assign weaker access privileges". Otherwise an object of the subclass could not be used safely through a reference of the parent type.
Do Java modules replace access modifiers?
No, modules work on top of them. The JVM first checks whether the module exports the package with an exports directive, and only then the ordinary rules for public, protected, package-private and private apply. The practical consequence: a class can be public and still be unreachable from another module. In projects without module-info.java the code lands in the unnamed module, where this extra check restricts nothing.
Summary
- Java has three access modifiers (
public,protected,private) and four access levels — the fourth, package-private, is the absence of a modifier. - Ordered from strictest to loosest:
private→ package-private →protected→public. protectedmeans "the whole package plus subclasses elsewhere", and outside the package it works only through a subclass-typed reference.- A top-level class can only be
publicor package-private; a nested class may use all four levels. - Overriding may widen the access level but never narrow it.
- Since Java 9 the module system adds an outer check: an unexported package keeps its
publicclasses invisible to other modules.
Comments