Java Access Modifier Scope Explained
java access modifier scope: Learn how public, private, protected, and package-private access modifiers control member visibility and how to choose the right scope in J...
java access modifier scope requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you declare a field or method in Java, you also decide how visible it is to other classes. That decision is the access modifier scope, and it affects everything from encapsulation to API stability. Java provides four access levels: public, protected, package-private (no modifier), and private. Each defines a distinct boundary for member access, and understanding these boundaries is essential for designing maintainable classes.
The Four Access Modifiers in Java
Java's access modifiers control where a member can be accessed from. The scope is not just about syntax; it defines a contract between the declaring class and the rest of the codebase. The four levels are:
public– accessible from any class in any package.protected– accessible from classes in the same package and subclasses (including those in different packages).- package-private (default) – accessible only from classes in the same package.
private– accessible only from the declaring class itself.
The table below summarizes the visibility rules:
| Modifier | Same class | Same package | Subclass (different package) | Any class |
|---|---|---|---|---|
public | Yes | Yes | Yes | Yes |
protected | Yes | Yes | Yes | No |
| package-private | Yes | Yes | No | No |
private | Yes | No | No | No |
Public Scope: Accessible Everywhere
A public member is part of the class's external API. Any code that can reference the class can access the member. This is appropriate for constants, factory methods, and interfaces that other modules need to call.
public class ApiClient { public static final String DEFAULT_BASE_URL = "https://api.example.com"; public void connect() { // implementation } }
Because public members are exposed to all callers, changing them later can break downstream code. Treat them as a commitment. If you expose a field directly, you lose control over its values and cannot enforce invariants without adding validation logic later.
Private Scope: Restricted to the Declaring Class
A private member is visible only within the class body. No other class, not even a subclass, can access it directly. This is the foundation of encapsulation: internal state and implementation details stay hidden.
public class BankAccount { private double balance; public void deposit(double amount) { if (amount <= 0) { throw new IllegalArgumentException("Amount must be positive"); } balance += amount; } public double getBalance() { return balance; } }
Here balance is private, so it can only be modified through the deposit method, which enforces a validation rule. If balance were public, any caller could set it to a negative value, breaking the class's invariants. Use private for fields that should never be directly accessed and for helper methods that are implementation details.
Protected Scope: Package and Subclass Access
The protected modifier sits between package-private and public. A protected member is accessible to all classes in the same package and to subclasses in any package. This is useful when you want to allow subclasses to customize behavior without exposing the member to the entire world.
public abstract class Shape { protected double area; protected abstract void calculateArea(); } public class Circle extends Shape { private double radius; public Circle(double radius) { this.radius = radius; calculateArea(); } @Override protected void calculateArea() { area = Math.PI * radius * radius; } }
In this example, area is protected so Circle can write to it, but external callers cannot. The calculateArea method is also protected, allowing subclasses to override it while keeping it out of the public API. Note that protected does not grant access to unrelated classes in the same package? Actually it does – same package classes can access protected members. So be careful: if a class is in the same package, it can access protected members even if it is not a subclass.
Package-Private (Default) Scope
When you omit an access modifier, the member has package-private visibility. It is accessible only from classes in the same package. This is the default and is often overlooked because it is not an explicit keyword.
class PackageHelper { static int counter = 0; static void increment() { counter++; } }
Package-private members are useful for internal collaboration between classes that are closely related but should not be exposed to the outside. For example, a package might contain a public facade and several internal helper classes that share state through package-private methods. This keeps the implementation hidden from other packages while allowing cohesive groups of classes to work together.
One subtlety: package-private is not the same as protected. A subclass in a different package cannot access a package-private member, even though it can access a protected one. This distinction is a common source of confusion.
Choosing the Right Access Modifier for a Member
The default instinct should be to use the most restrictive modifier that still allows the code to function. This principle, known as least privilege, reduces the surface area for bugs and unintended coupling. Here is a practical decision process:
- Start with
privatefor all fields and helper methods. - If a subclass in another package needs to access a member, change it to
protected. - If another class in the same package needs access, consider package-private before making it public.
- Only use
publicfor members that are part of the stable API you intend to support.
Consider the following scenario: you have a UserService class that needs to call a validateEmail method from a helper class in the same package. Making validateEmail package-private is sufficient. If you make it public, you expose an implementation detail that other packages might depend on, making future refactoring harder.
Common Access Modifier Mistakes and Their Consequences
Misapplying access modifiers leads to fragile code and subtle bugs. Here are the most frequent mistakes and why they matter.
Exposing Mutable Fields as Public
A public field can be changed from anywhere. If the field is a collection or a mutable object, callers can corrupt the internal state without going through any validation.
public class Order { public List<String> items = new ArrayList<>(); }
Any code can call order.items.add(...) without restrictions. A better design is to make the field private and provide controlled methods to add or remove items.
Using Protected When Package-Private Is Enough
protected expands visibility to all subclasses, including those in other packages. If you only need to share a member within the same package, package-private is more restrictive and safer. Using protected unnecessarily can lead to accidental coupling with subclasses that you do not control.
Forgetting the Default Modifier
Many developers assume that omitting the modifier means the member is accessible only within the class. In reality, it is package-private. This can cause unexpected access from other classes in the same package. Always be explicit about the intended visibility, even if that means writing private when you want class-only access.
Access Modifiers and Maintainability
The way you set access modifiers directly affects how easily you can evolve your code. A class with many public members is harder to change because each public member becomes part of the implicit contract with external code. On the other hand, a class that hides everything behind private methods and exposes only a few public entry points is easier to refactor internally.
When designing a library or a module, decide which members are part of the public API and document them. Keep everything else package-private or private. This separation lets you change internal implementations without breaking consumers. It also makes testing easier because you can test package-private methods directly from tests in the same package, without making them public.
Another maintainability concern is the use of protected in a class that is not designed for inheritance. If you mark a method protected but do not intend the class to be subclassed, you are opening a door that might not be needed. Prefer private or package-private unless you explicitly support extension. This reduces the number of assumptions other developers can make about your class's behavior.
Access modifier scope is not just a syntax detail; it is a design tool. Choosing the right level of visibility for each member is a deliberate act that shapes how your code can be used and modified. By consistently applying the least privilege principle, you keep your classes cohesive, your dependencies explicit, and your future refactoring safe.