Java Access Modifiers Deep Dive
Java Access Modifiers Deep Dive
Who should be able to see this? That is the question every access modifier answers. Java gives you four answers: everyone (public), nobody outside this class (private), this package and subclasses (protected), and this package only (default, also called package-private).
Access modifiers are the enforcement mechanism behind encapsulation — the first pillar of object-oriented programming. Without them, every field and method in every class would be visible to every other class in the entire program. Any code anywhere could reach in and corrupt your object's state. Access modifiers draw clear boundaries: here is the public surface (what callers use), here is the private interior (implementation details no one else touches), and here is everything in between.
The Four Access Modifiers at a Glance
VISIBILITY MATRIX — where is the member accessible?
Modifier │ Same Class │ Same Package │ Subclass │ Everywhere
───────────────┼────────────┼──────────────┼──────────────┼──────────────
public │ ✓ │ ✓ │ ✓ │ ✓
protected │ ✓ │ ✓ │ ✓ │ ✗
default │ ✓ │ ✓ │ ✗ │ ✗
private │ ✓ │ ✗ │ ✗ │ ✗
✓ = accessible ✗ = not accessible
Remember the order (most to least restrictive):
private → default → protected → public
↑
most restrictive least restrictive
Where Access Modifiers Can Be Applied
Access modifiers can be placed on different kinds of declarations — and not all four modifiers are valid everywhere.
On a TOP-LEVEL CLASS (not nested): ✓ public — visible to all ✓ default — visible within the package only ✗ private — not valid on top-level class ✗ protected — not valid on top-level class On a CLASS MEMBER (field, method, constructor, nested class): ✓ public ✓ protected ✓ default (no keyword — just omit the modifier) ✓ private Summary: Top-level classes → public or default only Members → all four modifiers available
1 — public: Accessible Everywhere
A public member is visible to every class in every package. The public modifier is used for the intentional, stable API surface of a class — the methods and constructors you want all callers to use.
// File: com/devstackflow/api/OrderApi.java
package com.devstackflow.api;
import java.time.LocalDate;
public class OrderApi {
// public field — visible everywhere (avoid for mutable state)
public static final String API_VERSION = "v2";
// public constructor — any code can create an OrderApi
public OrderApi() { }
// public method — part of the intended API surface
public String placeOrder(String customerId, double amount) {
String orderId = generateOrderId(); // calls private helper
validateAmount(amount); // calls private helper
return orderId;
}
public String getApiVersion() {
return API_VERSION;
}
// private helpers — NOT part of the public API
private String generateOrderId() {
return "ORD-" + System.currentTimeMillis();
}
private void validateAmount(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
}
}// File: com/differentpackage/Client.java
package com.differentpackage;
import com.devstackflow.api.OrderApi;
public class Client {
public static void main(String[] args) {
OrderApi api = new OrderApi(); // public constructor — accessible
System.out.println("API version : " + OrderApi.API_VERSION); // public field
String orderId = api.placeOrder("CUST-001", 1299.0); // public method
System.out.println("Order placed: " + orderId);
// api.generateOrderId() ← compile error — private method not accessible
// api.validateAmount(0) ← compile error — private method not accessible
}
}Output:
API version : v2
Order placed: ORD-1705312200000
2 — private: Accessible Only Within the Same Class
A private member is completely hidden from everything outside the class that declares it — including subclasses. It is the tightest boundary. Fields should almost always be private — they are implementation details, not contract. Private methods are implementation helpers.
// File: com/devstackflow/model/BankAccount.java
package com.devstackflow.model;
import java.util.ArrayList;
import java.util.List;
public class BankAccount {
// private fields — encapsulated, no direct external access
private final String accountId;
private final String holderId;
private double balance;
private final List<String> transactionHistory;
// public constructor — how the outside world creates an account
public BankAccount(String accountId, String holderId, double initialBalance) {
this.accountId = accountId;
this.holderId = holderId;
this.balance = initialBalance;
this.transactionHistory = new ArrayList<>();
recordTransaction("Account opened with Rs." + initialBalance);
}
// public API — controlled operations
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive.");
balance += amount;
recordTransaction("Deposit Rs." + amount);
}
public void withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive.");
if (amount > balance) throw new IllegalStateException("Insufficient funds.");
balance -= amount;
recordTransaction("Withdrawal Rs." + amount);
}
// public getters — read-only controlled access
public String getAccountId() { return accountId; }
public String getHolderId() { return holderId; }
public double getBalance() { return balance; }
public void printStatement() {
System.out.println("=== Statement: " + accountId + " ===");
transactionHistory.forEach(t -> System.out.println(" " + t));
System.out.printf(" Balance: Rs.%.2f%n", balance);
}
// private helper — internal implementation detail
private void recordTransaction(String description) {
transactionHistory.add("[" + java.time.LocalTime.now()
.withNano(0) + "] " + description);
}
}// File: BankAccountDemo.java
package com.devstackflow.model;
public class BankAccountDemo {
public static void main(String[] args) {
BankAccount account = new BankAccount("ACC-001", "Priya Sharma", 10000.0);
account.deposit(5000.0);
account.deposit(2500.0);
account.withdraw(3000.0);
account.printStatement();
// account.balance = 50000; ← compile error — private field
// account.recordTransaction("hack"); ← compile error — private method
// account.transactionHistory.clear(); ← compile error — private field
System.out.println("\nCurrent balance: Rs." + account.getBalance());
}
}Output:
=== Statement: ACC-001 ===
[10:30:00] Account opened with Rs.10000.0
[10:30:00] Deposit Rs.5000.0
[10:30:00] Deposit Rs.2500.0
[10:30:00] Withdrawal Rs.3000.0
Balance: Rs.14500.00
Current balance: Rs.14500.0
balance is private — no outside code can set it directly to any value. Every change goes through deposit() or withdraw(), which enforce the business rules. This is encapsulation in action.
3 — default (package-private): Accessible Within the Same Package
When no access modifier is written, the member gets default (package-private) visibility. It is accessible to all classes within the same package but invisible to classes in other packages — including subclasses in different packages.
// File: com/devstackflow/payment/PaymentValidator.java
package com.devstackflow.payment;
// No public — this class is package-private
// Only classes inside com.devstackflow.payment can use it
class PaymentValidator {
// package-private method
boolean isValidAmount(double amount) {
return amount > 0 && amount <= 100_000;
}
// package-private method
boolean isValidMerchant(String merchantId) {
return merchantId != null
&& !merchantId.isBlank()
&& merchantId.startsWith("MID-");
}
// package-private constant
static final double MAX_SINGLE_PAYMENT = 100_000.0;
}// File: com/devstackflow/payment/PaymentProcessor.java
package com.devstackflow.payment;
// Same package — can access PaymentValidator (package-private)
public class PaymentProcessor {
private final PaymentValidator validator = new PaymentValidator(); // OK — same package
public String process(String merchantId, double amount) {
// Can use validator because same package
if (!validator.isValidAmount(amount)) {
return "REJECTED: invalid amount Rs." + amount;
}
if (!validator.isValidMerchant(merchantId)) {
return "REJECTED: invalid merchant " + merchantId;
}
return "APPROVED: " + merchantId + " | Rs." + amount;
}
}// File: com/devstackflow/demo/PaymentDemo.java
package com.devstackflow.demo;
import com.devstackflow.payment.PaymentProcessor;
// import com.devstackflow.payment.PaymentValidator; ← compile error — package-private
public class PaymentDemo {
public static void main(String[] args) {
PaymentProcessor processor = new PaymentProcessor(); // public class — accessible
System.out.println(processor.process("MID-123", 5000.0));
System.out.println(processor.process("", 5000.0));
System.out.println(processor.process("MID-456", -100.0));
System.out.println(processor.process("MID-789", 200000.0));
// PaymentValidator validator = new PaymentValidator(); ← compile error
// Cannot access package-private class from outside its package
}
}Output:
APPROVED: MID-123 | Rs.5000.0
REJECTED: invalid merchant
REJECTED: invalid amount Rs.-100.0
REJECTED: invalid amount Rs.200000.0
PaymentValidator is an internal implementation class — a helper used only by PaymentProcessor and its package siblings. Making it package-private hides the implementation detail completely. Callers in com.devstackflow.demo only see and use PaymentProcessor.
4 — protected: Package Plus Inheritance
protected grants everything default grants (same-package access), plus access from subclasses in any package through inheritance. The subclass accesses the protected member through super or through an instance of the subclass itself — not through an arbitrary instance of the parent class from outside the package.
// File: com/devstackflow/base/Animal.java
package com.devstackflow.base;
public class Animal {
private String name; // private — subclasses cannot access directly
protected String species; // protected — subclasses and same package can access
public String type; // public — everyone can access
// protected constructor — subclasses (in any package) can call super()
protected Animal(String name, String species) {
this.name = name;
this.species = species;
this.type = "Animal";
}
// protected method — subclasses can call and override
protected String describe() {
return species + " named " + name;
}
// public method — anyone can call
public String getName() { return name; }
public String getType() { return type; }
}// File: com/devstackflow/zoo/Dog.java
package com.devstackflow.zoo; // DIFFERENT package from Animal
import com.devstackflow.base.Animal;
public class Dog extends Animal {
private String breed;
public Dog(String name, String breed) {
super(name, "Canis lupus familiaris"); // protected constructor accessible via super()
this.breed = breed;
this.type = "Pet"; // protected field accessible in subclass
}
@Override
protected String describe() {
// Can call super.describe() — protected method
return super.describe() + " | Breed: " + breed;
}
public void introduce() {
System.out.println("Hi, I am " + getName()); // public method
System.out.println("Species : " + species); // protected field — accessible
System.out.println("Type : " + type); // protected field — accessible
System.out.println("Details : " + describe()); // protected method
}
}// File: com/devstackflow/zoo/ZooDemo.java
package com.devstackflow.zoo;
import com.devstackflow.base.Animal;
public class ZooDemo {
public static void main(String[] args) {
Dog dog = new Dog("Bruno", "Labrador");
dog.introduce();
System.out.println();
// Accessing protected member through a NON-SUBCLASS in a different package
Animal animal = new Dog("Max", "Beagle");
// animal.species = "Modified"; ← compile error from outside the package
// protected via non-subclass ref in diff package
System.out.println("Type via public: " + animal.getType());
}
}Output:
Hi, I am Bruno
Species : Canis lupus familiaris
Type : Pet
Details : Canis lupus familiaris named Bruno | Breed: Labrador
Type via public: Pet
Access Modifiers — Complete Comparison Table
| Aspect | private | default | protected | public |
|---|---|---|---|---|
| Same class | ✓ | ✓ | ✓ | ✓ |
| Same package, different class | ✗ | ✓ | ✓ | ✓ |
| Subclass in same package | ✗ | ✓ | ✓ | ✓ |
| Subclass in different package | ✗ | ✗ | ✓ | ✓ |
| Non-subclass in different package | ✗ | ✗ | ✗ | ✓ |
| Valid on top-level class | ✗ | ✓ | ✗ | ✓ |
| Valid on member (field, method, constructor) | ✓ | ✓ | ✓ | ✓ |
| Keyword required | private | none (omit) | protected | public |
| Typical use — fields | Internal state | Package-scoped state | Inheritable state | Avoid mutable public fields |
| Typical use — methods | Helpers, validators | Package-internal ops | Extensible hooks | API surface |
| Typical use — constructors | Factory-only creation | Package creation | Subclass creation | General construction |
| Typical use — classes | Inner/nested only | Internal helper classes | Not common | API classes |
Combining Access Modifiers on Classes and Members
// File: AccessCombinationDemo.java
package com.devstackflow.demo;
public class AccessCombinationDemo {
// Field access modifiers
private int privateField = 1; // only this class
int defaultField = 2; // this package
protected int protectedField = 3; // this package + subclasses
public int publicField = 4; // everywhere
// Method access modifiers
private void privateMethod() { System.out.println("private"); }
void defaultMethod() { System.out.println("default"); }
protected void protectedMethod() { System.out.println("protected"); }
public void publicMethod() { System.out.println("public"); }
// Constructor access modifiers
// private — only static factory methods can create instances
// default — only same-package code can use new
// protected — subclasses from any package can call super()
// public — anyone can call new
public static void main(String[] args) {
AccessCombinationDemo obj = new AccessCombinationDemo();
// Inside the same class — all four are accessible
System.out.println("private field: " + obj.privateField);
System.out.println("default field: " + obj.defaultField);
System.out.println("protected field: " + obj.protectedField);
System.out.println("public field: " + obj.publicField);
obj.privateMethod();
obj.defaultMethod();
obj.protectedMethod();
obj.publicMethod();
}
}Output:
private field: 1
default field: 2
protected field: 3
public field: 4
private
default
protected
public
Inside the declaring class, all four modifiers are accessible. The restrictions only apply when accessing from outside the class.
Access Modifiers and Inheritance Rules
When overriding a method, the overriding method cannot reduce the visibility — it can only maintain or increase it.
// File: InheritanceAccessDemo.java
package com.devstackflow.demo;
class Parent {
public void publicMethod() { System.out.println("Parent: public"); }
protected void protectedMethod() { System.out.println("Parent: protected"); }
void defaultMethod() { System.out.println("Parent: default"); }
// private methods are NOT inherited — no override possible
}
class Child extends Parent {
// Maintaining same visibility — OK
@Override
public void publicMethod() { System.out.println("Child: public"); }
// Widening visibility — OK (protected → public)
@Override
public void protectedMethod() { System.out.println("Child: protected→public"); }
// Narrowing visibility — COMPILE ERROR
// @Override
// private void publicMethod() { } ← cannot reduce public to private
// default → public — OK (widening)
@Override
public void defaultMethod() { System.out.println("Child: default→public"); }
}
public class InheritanceAccessDemo {
public static void main(String[] args) {
Child child = new Child();
child.publicMethod();
child.protectedMethod();
child.defaultMethod();
System.out.println();
// Polymorphism still works — Parent ref, Child behaviour
Parent ref = new Child();
ref.publicMethod();
ref.protectedMethod();
ref.defaultMethod();
}
}Output:
Child: public
Child: protected→public
Child: default→public
Child: public
Child: protected→public
Child: default→public
The rule: you can widen access when overriding, but never narrow it. A public method in the parent cannot be made protected or private in the child. If callers expect to call a public method on the parent type, polymorphism guarantees the child version is also accessible.
Basic Example
public class Main {
static class Box {
private int privateField = 1; // this class only
int defaultField = 2; // package only (no keyword)
protected int protectedField = 3; // package + subclasses
public int publicField = 4; // everywhere
public int total() {
// All four are visible from inside the declaring class itself
return privateField + defaultField + protectedField + publicField;
}
}
public static void main(String[] args) {
Box box = new Box();
// From another class in the SAME file (acts like the same package),
// private is hidden but default/protected/public are reachable
System.out.println("default : " + box.defaultField);
System.out.println("protected : " + box.protectedField);
System.out.println("public : " + box.publicField);
System.out.println("total : " + box.total());
// box.privateField would not compile here — private is Box-only
}
}Output
default : 2 protected : 3 public : 4 total : 10
- ›
privateFieldcan only be read insideBoxitself —total()can see it, butmain()cannot. - ›
defaultFieldhas no keyword, so it is package-private — visible to any class sharing its package (in a real project, that means the same folder/packagestatement). - ›
protectedFieldbehaves likedefaultFieldhere, plus it would also be visible to a subclass ofBoxliving in a different package. - ›
publicFieldhas no restriction at all — any class, anywhere, can read or write it.
Best Practices
Start with private and widen only when necessary. The principle of least privilege: give every member the most restrictive access that still lets the program work. When a field can be private, make it private. When a method needs package access, use default. Only make something public when it is genuinely part of the intended API that external callers need.
Never use public for mutable instance fields. public double balance lets any caller write account.balance = -99999.0 — bypassing every validation. Always make fields private and provide controlled access through getter and setter methods. The one exception: public static final constants — immutable, so direct access is safe.
Use protected sparingly — prefer composition over inheritance. protected creates an implicit coupling between the parent class and every subclass. Any change to a protected member can break all subclasses. If a method is only useful internally and should not be part of the public API, consider making it private and passing data through constructor parameters or method arguments instead.
Respect the package as an access unit. Classes in the same package can trust each other. default access is not a mistake — it is a deliberate choice to make something available within a cohesive module but hidden from outside. Design packages as units of related, mutually-trusting code.
Common Mistakes
Mistake 1 — Making Fields public Instead of private
// BAD — balance can be set to any value from anywhere
public class Account {
public double balance; // anyone can do: account.balance = -999999;
}
// GOOD — balance only changes through validated operations
public class Account {
private double balance;
public void deposit(double amount) {
if (amount > 0) balance += amount; // validation enforced
}
public double getBalance() { return balance; }
}Mistake 2 — Narrowing Access When Overriding
class Base {
public void display() { System.out.println("Base"); }
}
class Child extends Base {
// @Override
// private void display() { } // compile error — cannot reduce public to private
@Override
protected void display() { } // also error — protected < public
@Override
public void display() { System.out.println("Child"); } // correct
}Mistake 3 — Assuming Sub-packages Share package-private Access
// package com.myapp.util — class Helper with package-private method
package com.myapp.util;
class Helper {
void assist() { System.out.println("helping"); }
}
// package com.myapp.util.io — a SUB-package
package com.myapp.util.io;
import com.myapp.util.Helper; // compile error — Helper is package-private
// Sub-packages are completely separate packages
// package-private visibility does NOT cross sub-package boundariesMistake 4 — Using default Access Unintentionally
// Forgot the modifier — this is DEFAULT (package-private), not public!
class PaymentService { // ← no 'public' — package-private class
void processPayment() { } // ← no modifier — package-private method
}
// From another package:
import com.myapp.PaymentService; // compile error — cannot access package-private class
// Fix: add 'public' if it is meant to be part of the API
public class PaymentService {
public void processPayment() { }
}Interview Questions
Q1. What are the four access modifiers in Java and what does each one control?
public — accessible from anywhere, no restrictions. protected — accessible within the same package and from any subclass in any package. default (no keyword) — accessible only within the same package, also called package-private. private — accessible only within the declaring class itself. The order from most to least restrictive is: private → default → protected → public. The choice of modifier expresses the intended visibility and forms the access-control part of encapsulation.
Q2. What is the difference between default and protected access in Java?
Both default and protected allow access within the same package. The difference is inheritance: protected additionally allows access from any subclass in any package. A protected method can be called in a subclass defined in a completely different package. A default method cannot — it is invisible to code in other packages, even subclasses. Practically, default is used for internal package helpers, while protected is used for extensible hooks intended for subclass implementation.
Q3. Can you reduce the visibility of a method when overriding it in a subclass?
No. Java enforces that an overriding method must have the same or wider visibility. A public method in the parent must remain public in the child. A protected method can be made public but not private or default. This rule exists to preserve the Liskov Substitution Principle — any caller that can call the method on a reference of the parent type must also be able to call it on a reference of the child type. Narrowing visibility would break polymorphism.
Q4. Which access modifiers can be applied to a top-level class?
Only public and default (no modifier). A top-level class declared public is visible everywhere. A top-level class with no modifier is package-private — usable only within its own package. private and protected are not valid on top-level classes — the compiler rejects them. These two modifiers are valid on nested/inner classes and on class members (fields, methods, constructors).
Q5. Why should fields typically be private in Java?
Making fields private is the foundation of encapsulation. With private fields, the only way to change an object's state is through the methods the class exposes — which can validate input, enforce business rules, maintain invariants, and log changes. A public field allows any code anywhere to set it to any value, bypassing all validation. Private fields also allow the internal representation to change (for example, from double to BigDecimal for currency) without breaking any external code, because external code was never allowed to access the field directly.
Q6. What is the difference between private access and package-private access for inner classes?
A private inner class is visible only within the enclosing class — not even to other classes in the same package. A package-private inner class (no modifier) is visible to all classes in the same package. Private inner classes are used for implementation helpers that are entirely internal to one class, such as a private iterator, a private builder, or a private state machine. Package-private inner classes are used when the inner class is shared across a few related classes in the same package but should not be exposed beyond it.
FAQs
Can a private method be tested with JUnit?
Not directly — private methods are invisible to test classes in other packages, and even in the same package. The standard approach is to test private methods indirectly through the public API that calls them. If a private method is complex enough to need its own test, that is a signal it should be extracted to a package-private or public method in a separate helper class. Reflection (method.setAccessible(true)) can access private methods, but this is generally considered poor practice for unit tests.
Can a class have both public and protected members?
Yes — a class can have members with any mix of access modifiers. A typical well-designed class has private fields, public getters and service methods, protected extensibility hooks for subclasses, and private internal helpers. The mix is deliberate — each modifier serves a specific role in the class's design.
Does private access apply to instances or classes?
private in Java is class-level, not instance-level. One instance of a class can access the private fields of another instance of the same class. This is why equals() implementations can directly access other.somePrivateField when other is an instance of the same class. The check is: is the accessing code inside the same class definition? If yes, private access is granted regardless of which instance the member belongs to.
What does package-private mean and why is it useful?
Package-private (the default modifier — absence of any modifier keyword) means visible within the same package only. It is useful for internal implementation classes, helpers, validators, and utilities that are shared across several classes in a module but should not be exposed as part of the public API. This is how you create a well-encapsulated package — a clean public surface that external callers see, backed by package-private helpers that are invisible outside the module.
Is there any way to enforce access control at runtime in Java?
Java's standard access control is enforced at compile time and partly at class-loading time by the JVM's access check. The Java module system (Java 9+) provides stronger runtime enforcement through module-info.java — a module can export only selected packages, making other packages completely inaccessible even via reflection in certain configurations. Within a single module or without modules, reflection with setAccessible(true) can bypass private access at runtime, though this is restricted by default in Java 16+.
Summary
Java's four access modifiers are the enforcement mechanism for encapsulation. private seals implementation details inside the declaring class — fields, helpers, and internal logic that no caller should touch. default creates a package-level boundary — trusted helpers visible to package siblings, hidden from the outside world. protected extends to subclasses — extensibility hooks that inheritance chains can use without exposing them to all callers. public is the deliberate API surface — what callers are meant to see and use.
The decision process: start with private. Widen to default if package siblings need it. Widen to protected if subclasses need it. Widen to public only when external callers genuinely need it. Never widen access beyond what is necessary — every unnecessary widening is a future maintenance liability.
What to Read Next
Learn what the public modifier allows other code to do.
Practice Programs
Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.
A class with a private field that validates every value before storing it.
A class with private fields accessed only through matching getter and setter methods.