Java Tutorial
🔍

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

Aspectprivatedefaultprotectedpublic
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 requiredprivatenone (omit)protectedpublic
Typical use — fieldsInternal statePackage-scoped stateInheritable stateAvoid mutable public fields
Typical use — methodsHelpers, validatorsPackage-internal opsExtensible hooksAPI surface
Typical use — constructorsFactory-only creationPackage creationSubclass creationGeneral construction
Typical use — classesInner/nested onlyInternal helper classesNot commonAPI 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
  1. ›privateField can only be read inside Box itself — total() can see it, but main() cannot.
  2. ›defaultField has no keyword, so it is package-private — visible to any class sharing its package (in a real project, that means the same folder/package statement).
  3. ›protectedField behaves like defaultField here, plus it would also be visible to a subclass of Box living in a different package.
  4. ›publicField has 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 boundaries

Mistake 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

Practice Programs

Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.

Browse all Java programs →