Key Takeaways
- Java has four access levels: public, protected, package-private, and private. Package-private means you omit an access modifier.
- Start with private members and expose only what callers need. Public fields create dependencies on your internal representation.
- Protected grants same-package access plus restricted access from subclasses in other packages; it does not let those subclasses access every superclass instance.
- Top-level classes and interfaces can be public or package-private. Member types declared inside classes can also be private or protected.
Java access modifiers control where your code can refer to types, constructors, methods, and fields. Choosing the narrowest suitable access level supports encapsulation: keeping implementation details behind a small, intentional interface.
The practical rule is simple: make members private first, then widen access for a concrete caller or extension point—not because you might need it later.
Java Access Levels at a Glance
This matrix describes ordinary access to class instance members, assuming the type you're using is accessible. “Same package” includes unrelated classes, not just subclasses.
| Access level | Declaring class | Other class in the same package | Subclass in another package | Unrelated class in another package |
|---|---|---|---|---|
| public | Yes | Yes | Yes | Yes |
| protected | Yes | Yes | Yes, with restrictions | No |
| package-private: no modifier | Yes | Yes | No | No |
| private | Yes | No | No | No |
Two details matter: nested types have special private-access relationships with their enclosing type, and cross-package protected instance access depends on the qualifying expression's compile-time type. Both are covered below.
A Runnable Example Across Two Packages
Create the following source tree. These examples use conventional classes and main methods and work with JDK 17 or later; no build tool or external dependency is required.
src/
mypackage/
ClassA.java
PackagePeer.java
otherpackage/
PublicCaller.java
MainClass.javaStart with a class that demonstrates all four access levels. The age is stored as an int; see Java primitive data types if that declaration is unfamiliar.
// src/mypackage/ClassA.java
package mypackage;
public class ClassA {
private int myAge;
public ClassA(int age) {
this.myAge = checkedAge(age);
}
public int age() {
return myAge;
}
public void changeAge(int age) {
this.myAge = checkedAge(age);
}
protected int inheritedAge() {
return myAge;
}
int packageAge() {
return myAge;
}
private static int checkedAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Age must not be negative");
}
return age;
}
}The accessors deliberately overlap to make the visibility rules easy to compare. A real class usually wouldn't need three ways to read the same value.
public: Expose Operations, Not Internal State
The public constructor, age(), and changeAge(int) are available to an unrelated caller in another package. The explicit import makes the dependency on ClassA clear.
// src/otherpackage/PublicCaller.java
package otherpackage;
import mypackage.ClassA;
public class PublicCaller {
public static void main(String[] args) {
ClassA person = new ClassA(43);
person.changeAge(44);
System.out.println(person.age()); // 44
// Uncomment one line at a time to see compilation errors:
// System.out.println(person.myAge); // private
// System.out.println(person.packageAge()); // package-private
// System.out.println(person.inheritedAge()); // protected
}
}When should you use public?
Use public for the interface you intentionally offer to callers outside the package. Avoid mutable public fields: callers can depend on their names and types, and direct assignment bypasses validation in your methods. Public fields create external dependencies that make later implementation changes harder.
private: Keep State and Validation Together
In ClassA, unrelated classes cannot directly access myAge or call checkedAge(int), even if they're in mypackage. Both the constructor and changeAge(int) validate the proposed value before assigning it.
For example, changeAge(-1) throws IllegalArgumentException without replacing the previous age. The guarantee comes from the validation code—not from the private keyword alone.
When should you use private?
Use private for implementation details unless another caller genuinely needs access. A private field doesn't require a getter or setter; sometimes the right interface exposes only meaningful operations, or no access at all.
Private doesn't mean “only the exact class declaration, with no exceptions.” Code inside the same enclosing top-level class can access private members of its nested classes, and nested classes can access private members of the enclosing class.
Package-Private: Share Within a Package
The packageAge() method has package-private access because its declaration has no access modifier. PackagePeer doesn't extend ClassA, but it can call both packageAge() and inheritedAge() because it belongs to the same package.
// src/mypackage/PackagePeer.java
package mypackage;
class PackagePeer {
public static void main(String[] args) {
ClassA person = new ClassA(43);
System.out.println(person.age()); // 43
System.out.println(person.packageAge()); // 43
System.out.println(person.inheritedAge()); // 43
// System.out.println(person.myAge); // private: does not compile
}
}PackagePeer itself is also package-private. Classes in other packages cannot directly name or import this top-level type.
When should you use package-private?
Use it for helpers and collaboration between classes that implement one package's behavior. It is narrower than public, but broader than private: every class in that package can use the member. Subpackages don't get special access; mypackage.tools is a different package from mypackage.
protected: Understand the Cross-Package Restriction
Protected includes same-package access. Outside that package, access must occur in subclass code; for an instance member accessed through an expression, that expression's compile-time type must be the accessing subclass or one of its subclasses.
// src/otherpackage/MainClass.java
package otherpackage;
import mypackage.ClassA;
public class MainClass extends ClassA {
public MainClass(int age) {
super(age);
}
private void demonstrate(MainClass other, ClassA parent) {
System.out.println(this.inheritedAge()); // 43
System.out.println(other.inheritedAge()); // 44
// Does not compile: parent is declared as ClassA.
// System.out.println(parent.inheritedAge());
// These also do not compile:
// System.out.println(this.packageAge()); // different package
// System.out.println(this.myAge); // private
}
public static void main(String[] args) {
MainClass self = new MainClass(43);
self.demonstrate(new MainClass(44), self);
}
}Notice that parent receives self, so the actual object is a MainClass. That doesn't make parent.inheritedAge() legal: the reference is declared as ClassA. If myAge were a protected field instead, this.myAge would be legal here, but parent.myAge would have the same qualifier problem.
When should you use protected?
Use protected for deliberate subclass extension points. Prefer a protected operation over exposing mutable fields, and remember that unrelated classes in the same package also receive access. Protected is not “subclasses only.”
Compile and Run the Examples
From the directory containing src, run:
javac -d out src/mypackage/ClassA.java src/mypackage/PackagePeer.java src/otherpackage/PublicCaller.java src/otherpackage/MainClass.java
java -cp out otherpackage.PublicCaller
java -cp out mypackage.PackagePeer
java -cp out otherpackage.MainClassPublicCaller prints 44; PackagePeer prints 43 three times; MainClass prints 43 and then 44. Leave the intentionally invalid lines commented out for the complete project to compile.
The javac -d option places compiled classes in the output directory's package structure. The java -cp option supplies the classpath, and the following argument names the class to launch. If startup fails, check the classpath root and fully qualified name using our guide to the Java “could not find or load main class” error.
Top-Level Types, Nested Types, and Constructors
A top-level class or interface can be public or package-private, but not private or protected. A member class or interface declared inside a class can use all four access levels. Local classes declared inside methods don't take access modifiers.
This standalone declaration example shows a public top-level class, a private nested class, and a package-private helper. It compiles but has no main method:
// Container.java
public class Container {
private static class State {
private int count;
}
private final State state = new State();
public int count() {
return state.count;
}
}
class PackageHelper {
}Container can read State.count despite that field being private. For the distinction between static nested classes and inner classes, see our guide to Java nested classes.
Constructors have access levels too: a public class can have a private constructor. For an ordinary class, an explicitly declared constructor without an access modifier is package-private; if you declare no constructors, the implicitly generated default constructor has the class's access level. These are different cases. The JLS chapter on classes defines the constructor rules.
Public Access Still Has Type and Module Boundaries
A public member doesn't make its containing type public. For example, another package cannot directly name PackagePeer just because its main method is public.
Named modules add another boundary: ordinary access from a different module requires the relevant module readability relationship and an export of the package to that caller. Public alone doesn't export a package. An opens directive instead enables reflective access; it doesn't grant ordinary source-code access to private members. The examples above use the classpath rather than named modules.
Choosing Visibility for Maintainable APIs
Public and protected members are compatibility commitments: callers or subclasses may depend on them. Treat widening access as an API-design decision, especially for a published library. Package-private helpers keep implementation collaboration inside a package; private members leave you the most freedom to change internals.
In a library inventory system, keep the available-copy count private and expose operations such as borrowing and returning a copy. Those operations should validate the transition—for example, refusing a borrow when no copies remain—rather than offering unrestricted assignment to a public count.
Common Mistakes
- Making everything public to fix compiler errors. First decide whether the caller belongs behind the same interface or whether you need a narrow public operation.
- Using protected as a substitute for private. It also exposes members to unrelated same-package classes.
- Expecting imports to grant permission. Imports shorten accessible type names; they don't change access levels.
- Narrowing access when overriding. An overriding method cannot provide less access than the method it overrides.
The language rules enforce the last two distinctions independently of how you organize source files.
FAQ
What are the four access levels in Java?
Java's four access levels are public, protected, package-private, and private. Package-private is expressed by omitting an access modifier; the other three have explicit keywords.
Is default an access modifier in Java?
No. “Default access” is another name for package-private access, which you request by writing no access modifier. The default keyword on an interface method declares a public method with an implementation, not package-private visibility.
Can a top-level Java class be private or protected?
No. A top-level class can be public or package-private. A member class declared inside another class can be private or protected.
Can a subclass in another package access a protected field through a parent-class reference?
Not through a reference whose compile-time type is the superclass. In that subclass's code, qualified access to a protected instance field requires a reference typed as the accessing subclass or one of its subclasses; the object's runtime type doesn't change that rule.
Should Java fields be public or private?
Keep fields private by default and expose only the operations callers need. Mutable public fields create external dependencies and allow assignment without your method-level validation; protected operations are preferable when you intentionally support subclass access.
Further Reading
- Dev.java: Controlling access to class members — visibility basics and choosing a restrictive access level.
- JLS Chapter 6: Access control — precise accessibility rules, including protected qualifiers.
- JLS Chapter 7: Packages and modules — top-level types, imports, exports, and opens.
- JLS Chapter 9: Interfaces — implicit access levels and default methods.
- The javac command — compiling source files and choosing the output directory.
- The java command — classpaths and launching compiled classes.
- AccessibleObject API documentation — reflective access checks and module restrictions.
- Secure Coding Guidelines for Java SE — validation, mutable state, and serialization hazards.
