Back to Blog
Java

Java Access Modifiers Explained

java access modifiers: Understand Java access modifiers: public, private, protected, and package-private. Learn their scope, inheritance behavior, and how to choose th...

Javaaccess modifiersencapsulationvisibilityinheritancepackage-private
Illustration of Java access modifiers showing public, protected, package-private, and private visibility scopes with a class diagram metaphor.

When you declare a class, method, or field in Java, you must decide how visible it is to other parts of the program. That decision is made with java access modifiers: public, protected, and private, plus the default package-private level when no modifier is written. The choice directly affects encapsulation, API design, and how easily the code can be changed later.

The Four Access Levels and Their Scope

Java defines four levels of access, each with a distinct visibility range. The table below summarizes them for a member declared in a class.

ModifierSame classSame packageSubclass (different package)Any class
publicYesYesYesYes
protectedYesYesYesNo
(none)YesYesNoNo
privateYesNoNoNo

The default (package-private) level is not a keyword; it is simply the absence of a modifier. This is a common source of confusion because many developers expect the default to be private, but it is actually visible to all classes in the same package.

How Access Modifiers Affect Inheritance and Overriding

Access modifiers interact with inheritance in ways that often surprise developers. A subclass can only override a method if the overriding method has the same or more permissive access. For example, a protected method can be overridden as protected or public, but not as private or package-private. This rule exists to preserve the contract that the parent class established.

Consider this example:

public class Parent { protected void greet() { System.out.println("Hello from Parent"); } } public class Child extends Parent { @Override public void greet() { // allowed: widening access System.out.println("Hello from Child"); } } n``` If you try to narrow the access in the override, the compiler rejects it. This rule prevents a subclass from silently hiding a method that callers of the parent class rely on. ## Access Modifiers on Members vs. Classes A top-level class can only be `public` or package-private. You cannot declare a top-level class as `private` or `protected`. The `private` and `protected` modifiers are reserved for nested classes and members. This distinction is important when designing APIs: a `public` top-level class is part of the exported surface, while a package-private class is an implementation detail that can change without breaking external callers. Nested classes follow the same visibility rules as other members. A `private` nested class is only accessible from the enclosing class, which is useful for hiding helper structures. A `protected` nested class is visible to subclasses, allowing them to use the nested type in their own implementation. ## Common Mistakes and Misunderstandings One frequent mistake is assuming that `protected` means "visible to all subclasses regardless of package." That is true, but only for subclasses that actually extend the class. A non-subclass in the same package also sees `protected` members because package visibility is included. Another mistake is using `public` fields when a getter would allow future validation or logging. Exposing fields directly makes it impossible to change the internal representation without breaking callers. Another subtle issue arises with overriding and access. If a parent method is `public` and the child overrides it as `protected`, the code will not compile. The compiler enforces the widening rule, but developers often encounter this when refactoring an interface implementation into a class hierarchy. ## Choosing the Right Access Level for Maintainability The primary goal of access modifiers is to control what other code can depend on. A smaller public surface makes it easier to evolve the implementation. As a rule of thumb, start with the most restrictive level that works: `private` for fields and helper methods, `package-private` for internal collaboration within a package, `protected` for extension points, and `public` only for the API you intend to support. This approach has a direct effect on maintainability. When a field is `private`, you can change its type, add constraints, or compute it lazily without affecting code outside the class. When a method is `package-private`, you can refactor it as long as you keep the same package. Overly broad visibility forces you to consider every external caller before making a change, which increases the cost of refactoring. ## Interaction with Interfaces and Records Interfaces and records have their own access rules. In an interface, every member is implicitly `public` unless the interface itself is package-private. Since Java 9, you can declare `private` methods in an interface to share code between default methods, but those private methods are not part of the public contract. Records, introduced in Java 16, have implicit accessors that are `public`, and their fields are `private final`. You cannot make a record's component `protected` or package-private; the accessor is always `public`. This design preserves the value-based semantics of records. When implementing an interface, the implementing method must be `public` because the interface method is public. If you try to implement it with a narrower modifier, the compiler rejects it. This is a common error when developers are used to package-private implementations in other contexts. ## Access Modifiers and Reflection Reflection can bypass access checks at runtime, but this is not a license to ignore modifiers. The `setAccessible(true)` call on a `Field` or `Method` can access private members, but it is fragile and can break under a security manager or in a modular environment. Relying on reflection to reach private state is a code smell that often indicates a design flaw. If you need to test private methods, consider extracting them into a package-private helper class instead of using reflection. From a security perspective, access modifiers are not a security boundary. They are a compile-time contract for developers, not a runtime protection mechanism. Malicious code can use reflection or unsafe APIs to access private data. Therefore, never store sensitive information in a field and assume that `private` protects it from all access. ## Practical Example: Building a Small API Consider a simple `BankAccount` class. The balance must be read but not directly modified. Using `private` for the balance and a `public` method to deposit money enforces the invariant that the balance cannot become negative. ```java public class BankAccount { private double balance; public BankAccount(double initialBalance) { if (initialBalance < 0) { throw new IllegalArgumentException("Initial balance cannot be negative"); } this.balance = initialBalance; } public void deposit(double amount) { if (amount <= 0) { throw new IllegalArgumentException("Deposit amount must be positive"); } this.balance += amount; } public double getBalance() { return balance; } }

Here, balance is private, so no external code can set it to an invalid value. The deposit method validates the amount before changing the state. This is the core benefit of access modifiers: they let you control the state transitions of your objects.

If you later need to add a withdrawal method, you can do so inside the class without affecting any existing caller. The public surface remains stable, and the internal logic can evolve independently.

java access modifiers: Practical Usage and Code Examples | RYUSLOG DEV