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.
1// File: com/devstackflow/api/OrderApi.java
2package com.devstackflow.api;
3
4import java.time.LocalDate;
5
6public class OrderApi {
7
8 // public field — visible everywhere (avoid for mutable state)
9 public static final String API_VERSION = "v2";
10
11 // public constructor — any code can create an OrderApi
12 public OrderApi() { }
13
14 // public method — part of the intended API surface
15 public String placeOrder(String customerId, double amount) {
16 String orderId = generateOrderId(); // calls private helper
17 validateAmount(amount); // calls private helper
18 return orderId;
19 }
20
21 public String getApiVersion() {
22 return API_VERSION;
23 }
24
25 // private helpers — NOT part of the public API
26 private String generateOrderId() {
27 return "ORD-" + System.currentTimeMillis();
28 }
29
30 private void validateAmount(double amount) {
31 if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
32 }
33}1// File: com/differentpackage/Client.java
2package com.differentpackage;
3
4import com.devstackflow.api.OrderApi;
5
6public class Client {
7
8 public static void main(String[] args) {
9
10 OrderApi api = new OrderApi(); // public constructor — accessible
11
12 System.out.println("API version : " + OrderApi.API_VERSION); // public field
13 String orderId = api.placeOrder("CUST-001", 1299.0); // public method
14 System.out.println("Order placed: " + orderId);
15
16 // api.generateOrderId() ← compile error — private method not accessible
17 // api.validateAmount(0) ← compile error — private method not accessible
18 }
19}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.
1// File: com/devstackflow/model/BankAccount.java
2package com.devstackflow.model;
3
4import java.util.ArrayList;
5import java.util.List;
6
7public class BankAccount {
8
9 // private fields — encapsulated, no direct external access
10 private final String accountId;
11 private final String holderId;
12 private double balance;
13 private final List<String> transactionHistory;
14
15 // public constructor — how the outside world creates an account
16 public BankAccount(String accountId, String holderId, double initialBalance) {
17 this.accountId = accountId;
18 this.holderId = holderId;
19 this.balance = initialBalance;
20 this.transactionHistory = new ArrayList<>();
21 recordTransaction("Account opened with Rs." + initialBalance);
22 }
23
24 // public API — controlled operations
25 public void deposit(double amount) {
26 if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive.");
27 balance += amount;
28 recordTransaction("Deposit Rs." + amount);
29 }
30
31 public void withdraw(double amount) {
32 if (amount <= 0) throw new IllegalArgumentException("Amount must be positive.");
33 if (amount > balance) throw new IllegalStateException("Insufficient funds.");
34 balance -= amount;
35 recordTransaction("Withdrawal Rs." + amount);
36 }
37
38 // public getters — read-only controlled access
39 public String getAccountId() { return accountId; }
40 public String getHolderId() { return holderId; }
41 public double getBalance() { return balance; }
42
43 public void printStatement() {
44 System.out.println("=== Statement: " + accountId + " ===");
45 transactionHistory.forEach(t -> System.out.println(" " + t));
46 System.out.printf(" Balance: Rs.%.2f%n", balance);
47 }
48
49 // private helper — internal implementation detail
50 private void recordTransaction(String description) {
51 transactionHistory.add("[" + java.time.LocalTime.now()
52 .withNano(0) + "] " + description);
53 }
54}1// File: BankAccountDemo.java
2package com.devstackflow.model;
3
4public class BankAccountDemo {
5
6 public static void main(String[] args) {
7
8 BankAccount account = new BankAccount("ACC-001", "Priya Sharma", 10000.0);
9
10 account.deposit(5000.0);
11 account.deposit(2500.0);
12 account.withdraw(3000.0);
13
14 account.printStatement();
15
16 // account.balance = 50000; ← compile error — private field
17 // account.recordTransaction("hack"); ← compile error — private method
18 // account.transactionHistory.clear(); ← compile error — private field
19
20 System.out.println("\nCurrent balance: Rs." + account.getBalance());
21 }
22}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.
1// File: com/devstackflow/payment/PaymentValidator.java
2package com.devstackflow.payment;
3
4// No public — this class is package-private
5// Only classes inside com.devstackflow.payment can use it
6class PaymentValidator {
7
8 // package-private method
9 boolean isValidAmount(double amount) {
10 return amount > 0 && amount <= 100_000;
11 }
12
13 // package-private method
14 boolean isValidMerchant(String merchantId) {
15 return merchantId != null
16 && !merchantId.isBlank()
17 && merchantId.startsWith("MID-");
18 }
19
20 // package-private constant
21 static final double MAX_SINGLE_PAYMENT = 100_000.0;
22}1// File: com/devstackflow/payment/PaymentProcessor.java
2package com.devstackflow.payment;
3
4// Same package — can access PaymentValidator (package-private)
5public class PaymentProcessor {
6
7 private final PaymentValidator validator = new PaymentValidator(); // OK — same package
8
9 public String process(String merchantId, double amount) {
10
11 // Can use validator because same package
12 if (!validator.isValidAmount(amount)) {
13 return "REJECTED: invalid amount Rs." + amount;
14 }
15 if (!validator.isValidMerchant(merchantId)) {
16 return "REJECTED: invalid merchant " + merchantId;
17 }
18 return "APPROVED: " + merchantId + " | Rs." + amount;
19 }
20}1// File: com/devstackflow/demo/PaymentDemo.java
2package com.devstackflow.demo;
3
4import com.devstackflow.payment.PaymentProcessor;
5// import com.devstackflow.payment.PaymentValidator; ← compile error — package-private
6
7public class PaymentDemo {
8
9 public static void main(String[] args) {
10
11 PaymentProcessor processor = new PaymentProcessor(); // public class — accessible
12
13 System.out.println(processor.process("MID-123", 5000.0));
14 System.out.println(processor.process("", 5000.0));
15 System.out.println(processor.process("MID-456", -100.0));
16 System.out.println(processor.process("MID-789", 200000.0));
17
18 // PaymentValidator validator = new PaymentValidator(); ← compile error
19 // Cannot access package-private class from outside its package
20 }
21}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.
1// File: com/devstackflow/base/Animal.java
2package com.devstackflow.base;
3
4public class Animal {
5
6 private String name; // private — subclasses cannot access directly
7 protected String species; // protected — subclasses and same package can access
8 public String type; // public — everyone can access
9
10 // protected constructor — subclasses (in any package) can call super()
11 protected Animal(String name, String species) {
12 this.name = name;
13 this.species = species;
14 this.type = "Animal";
15 }
16
17 // protected method — subclasses can call and override
18 protected String describe() {
19 return species + " named " + name;
20 }
21
22 // public method — anyone can call
23 public String getName() { return name; }
24 public String getType() { return type; }
25}1// File: com/devstackflow/zoo/Dog.java
2package com.devstackflow.zoo; // DIFFERENT package from Animal
3
4import com.devstackflow.base.Animal;
5
6public class Dog extends Animal {
7
8 private String breed;
9
10 public Dog(String name, String breed) {
11 super(name, "Canis lupus familiaris"); // protected constructor accessible via super()
12 this.breed = breed;
13 this.type = "Pet"; // protected field accessible in subclass
14 }
15
16 @Override
17 protected String describe() {
18 // Can call super.describe() — protected method
19 return super.describe() + " | Breed: " + breed;
20 }
21
22 public void introduce() {
23 System.out.println("Hi, I am " + getName()); // public method
24 System.out.println("Species : " + species); // protected field — accessible
25 System.out.println("Type : " + type); // protected field — accessible
26 System.out.println("Details : " + describe()); // protected method
27 }
28}1// File: com/devstackflow/zoo/ZooDemo.java
2package com.devstackflow.zoo;
3
4import com.devstackflow.base.Animal;
5
6public class ZooDemo {
7
8 public static void main(String[] args) {
9
10 Dog dog = new Dog("Bruno", "Labrador");
11 dog.introduce();
12
13 System.out.println();
14
15 // Accessing protected member through a NON-SUBCLASS in a different package
16 Animal animal = new Dog("Max", "Beagle");
17 // animal.species = "Modified"; ← compile error from outside the package
18 // protected via non-subclass ref in diff package
19 System.out.println("Type via public: " + animal.getType());
20 }
21}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
1// File: AccessCombinationDemo.java
2package com.devstackflow.demo;
3
4public class AccessCombinationDemo {
5
6 // Field access modifiers
7 private int privateField = 1; // only this class
8 int defaultField = 2; // this package
9 protected int protectedField = 3; // this package + subclasses
10 public int publicField = 4; // everywhere
11
12 // Method access modifiers
13 private void privateMethod() { System.out.println("private"); }
14 void defaultMethod() { System.out.println("default"); }
15 protected void protectedMethod() { System.out.println("protected"); }
16 public void publicMethod() { System.out.println("public"); }
17
18 // Constructor access modifiers
19 // private — only static factory methods can create instances
20 // default — only same-package code can use new
21 // protected — subclasses from any package can call super()
22 // public — anyone can call new
23
24 public static void main(String[] args) {
25 AccessCombinationDemo obj = new AccessCombinationDemo();
26
27 // Inside the same class — all four are accessible
28 System.out.println("private field: " + obj.privateField);
29 System.out.println("default field: " + obj.defaultField);
30 System.out.println("protected field: " + obj.protectedField);
31 System.out.println("public field: " + obj.publicField);
32
33 obj.privateMethod();
34 obj.defaultMethod();
35 obj.protectedMethod();
36 obj.publicMethod();
37 }
38}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.
1// File: InheritanceAccessDemo.java
2package com.devstackflow.demo;
3
4class Parent {
5 public void publicMethod() { System.out.println("Parent: public"); }
6 protected void protectedMethod() { System.out.println("Parent: protected"); }
7 void defaultMethod() { System.out.println("Parent: default"); }
8 // private methods are NOT inherited — no override possible
9}
10
11class Child extends Parent {
12
13 // Maintaining same visibility — OK
14 @Override
15 public void publicMethod() { System.out.println("Child: public"); }
16
17 // Widening visibility — OK (protected → public)
18 @Override
19 public void protectedMethod() { System.out.println("Child: protected→public"); }
20
21 // Narrowing visibility — COMPILE ERROR
22 // @Override
23 // private void publicMethod() { } ← cannot reduce public to private
24
25 // default → public — OK (widening)
26 @Override
27 public void defaultMethod() { System.out.println("Child: default→public"); }
28}
29
30public class InheritanceAccessDemo {
31
32 public static void main(String[] args) {
33 Child child = new Child();
34 child.publicMethod();
35 child.protectedMethod();
36 child.defaultMethod();
37
38 System.out.println();
39
40 // Polymorphism still works — Parent ref, Child behaviour
41 Parent ref = new Child();
42 ref.publicMethod();
43 ref.protectedMethod();
44 ref.defaultMethod();
45 }
46}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.
Real-World Example — Employee Management System
The Business Problem
An HR platform at a company like Infosys or TCS manages employee data with multiple tiers of visibility. Personal data like salary and bank details are private. Departmental data is default — accessible within the HR package only. Manager-specific operations are protected so team leaders can extend base behaviour. The public API is what the frontend and other services call.
1// File: com/hrplatform/model/Employee.java
2package com.hrplatform.model;
3
4import java.time.LocalDate;
5
6public class Employee {
7
8 // private — personal data, no direct external access
9 private final String employeeId;
10 private final String name;
11 private double salary;
12 private String bankAccountNumber;
13
14 // default — accessible within com.hrplatform.model only
15 LocalDate joiningDate;
16 String department;
17 int performanceRating; // 1-5 scale
18
19 // protected — accessible by subclasses (Manager, Contractor, etc.)
20 protected String designation;
21 protected String reportingTo;
22
23 // public — safe read-only access for all consumers
24 public final String email;
25
26 public Employee(String employeeId, String name,
27 String email, double salary,
28 String department, String designation) {
29 this.employeeId = employeeId;
30 this.name = name;
31 this.email = email;
32 this.salary = salary;
33 this.department = department;
34 this.designation = designation;
35 this.joiningDate = LocalDate.now();
36 this.bankAccountNumber = "XXXX" + employeeId.hashCode() % 10000;
37 this.performanceRating = 3;
38 }
39
40 // public getters — controlled read access
41 public String getEmployeeId() { return employeeId; }
42 public String getName() { return name; }
43 public double getSalary() { return salary; }
44 public String getDepartment() { return department; }
45
46 // public method — business operation
47 public String getSummary() {
48 return String.format("[%s] %s | %s | %s | Rs.%.0f",
49 employeeId, name, designation, department, salary);
50 }
51
52 // private helper — internal
53 private boolean isEligibleForBonus() {
54 return performanceRating >= 4;
55 }
56
57 // protected — only subclasses and same-package can use
58 protected void applyPromotion(String newDesignation, double incrementPercent) {
59 this.designation = newDesignation;
60 this.salary += salary * incrementPercent / 100.0;
61 System.out.println("Promoted: " + name + " → " + newDesignation
62 + " | New salary: Rs." + (int) salary);
63 }
64
65 // default — only same package can call this directly
66 void updateRating(int rating) {
67 if (rating < 1 || rating > 5)
68 throw new IllegalArgumentException("Rating must be 1-5.");
69 this.performanceRating = rating;
70 }
71
72 // public — all callers can check eligibility (but cannot see rating directly)
73 public boolean checkBonusEligibility() {
74 return isEligibleForBonus(); // calls private method internally
75 }
76}1// File: com/hrplatform/model/Manager.java
2package com.hrplatform.model;
3
4import java.util.ArrayList;
5import java.util.List;
6
7public class Manager extends Employee {
8
9 private final List<Employee> teamMembers = new ArrayList<>();
10
11 public Manager(String employeeId, String name,
12 String email, double salary, String department) {
13 super(employeeId, name, email, salary, department, "Manager");
14 this.reportingTo = "Director"; // protected field — accessible in subclass
15 }
16
17 public void addTeamMember(Employee emp) {
18 teamMembers.add(emp);
19 emp.reportingTo = this.getName(); // protected field — accessible in subclass ref
20 }
21
22 public void conductReview(Employee emp, int rating) {
23 emp.updateRating(rating); // default method — same package (com.hrplatform.model)
24 System.out.println("Review done: " + emp.getName() + " → rating " + rating);
25 if (emp.checkBonusEligibility()) {
26 System.out.println(" " + emp.getName() + " is eligible for bonus.");
27 }
28 }
29
30 public void promoteEmployee(Employee emp, String newTitle, double hike) {
31 emp.applyPromotion(newTitle, hike); // protected method — accessible from subclass
32 }
33
34 public void printTeam() {
35 System.out.println("=== Team of " + getName() + " ===");
36 teamMembers.forEach(e -> System.out.println(" " + e.getSummary()));
37 System.out.println("Reporting to: " + reportingTo); // protected field
38 }
39}1// File: com/hrplatform/HRDemo.java
2package com.hrplatform;
3
4import com.hrplatform.model.Employee;
5import com.hrplatform.model.Manager;
6
7public class HRDemo {
8
9 public static void main(String[] args) {
10
11 System.out.println("╔══════════════════════════════════════════╗");
12 System.out.println("║ HR PLATFORM — ACCESS MODIFIERS DEMO ║");
13 System.out.println("╚══════════════════════════════════════════╝\n");
14
15 Manager mgr = new Manager("EMP-001", "Priya Sharma",
16 "priya@infosys.com", 120000, "Engineering");
17
18 Employee e1 = new Employee("EMP-002", "Rohan Mehta",
19 "rohan@infosys.com", 75000, "Engineering", "SDE-1");
20 Employee e2 = new Employee("EMP-003", "Sneha Rao",
21 "sneha@infosys.com", 85000, "Engineering", "SDE-2");
22 Employee e3 = new Employee("EMP-004", "Karan Singh",
23 "karan@infosys.com", 70000, "Engineering", "SDE-1");
24
25 mgr.addTeamMember(e1);
26 mgr.addTeamMember(e2);
27 mgr.addTeamMember(e3);
28
29 System.out.println("=== Team Roster ===");
30 mgr.printTeam();
31
32 System.out.println("\n=== Annual Reviews ===");
33 mgr.conductReview(e1, 3);
34 mgr.conductReview(e2, 5);
35 mgr.conductReview(e3, 4);
36
37 System.out.println("\n=== Promotions ===");
38 mgr.promoteEmployee(e2, "SDE-3", 20.0);
39 mgr.promoteEmployee(e3, "SDE-2", 15.0);
40
41 System.out.println("\n=== Updated Team ===");
42 mgr.printTeam();
43
44 System.out.println("\n=== Public API (available to all callers) ===");
45 System.out.println("Name : " + e1.getName());
46 System.out.println("Email : " + e1.email); // public field
47 System.out.println("Summary : " + e1.getSummary());
48 System.out.println("Bonus? : " + e1.checkBonusEligibility());
49
50 // These would cause compile errors from com.hrplatform (different package):
51 // e1.salary = 99999; ← private field
52 // e1.department = "Finance"; ← default field (different package)
53 // e1.updateRating(5); ← default method (different package)
54 // e1.bankAccountNumber; ← private field
55 }
56}Output:
╔══════════════════════════════════════════╗
║ HR PLATFORM — ACCESS MODIFIERS DEMO ║
╚══════════════════════════════════════════╝
=== Team Roster ===
=== Team of Priya Sharma ===
[EMP-002] Rohan Mehta | SDE-1 | Engineering | Rs.75000
[EMP-003] Sneha Rao | SDE-2 | Engineering | Rs.85000
[EMP-004] Karan Singh | SDE-1 | Engineering | Rs.70000
Reporting to: Director
=== Annual Reviews ===
Review done: Rohan Mehta → rating 3
Review done: Sneha Rao → rating 5
Sneha Rao is eligible for bonus.
Review done: Karan Singh → rating 4
Karan Singh is eligible for bonus.
=== Promotions ===
Promoted: Sneha Rao → SDE-3 | New salary: Rs.102000
Promoted: Karan Singh → SDE-2 | New salary: Rs.80500
=== Updated Team ===
=== Team of Priya Sharma ===
[EMP-002] Rohan Mehta | SDE-1 | Engineering | Rs.75000
[EMP-003] Sneha Rao | SDE-3 | Engineering | Rs.102000
[EMP-004] Karan Singh | SDE-2 | Engineering | Rs.80500
Reporting to: Director
=== Public API (available to all callers) ===
Name : Rohan Mehta
Email : rohan@infosys.com
Summary : [EMP-002] Rohan Mehta | SDE-1 | Engineering | Rs.75000
Bonus? : false
Four access levels, four clear roles: private protects salary and bank details from any external mutation. default lets the Manager (in the same package) run reviews using updateRating(). protected lets the Manager subclass (any package) call applyPromotion() and access designation. public exposes the safe read-only surface that external systems like the frontend consume.
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
1// BAD — balance can be set to any value from anywhere
2public class Account {
3 public double balance; // anyone can do: account.balance = -999999;
4}
5
6// GOOD — balance only changes through validated operations
7public class Account {
8 private double balance;
9 public void deposit(double amount) {
10 if (amount > 0) balance += amount; // validation enforced
11 }
12 public double getBalance() { return balance; }
13}Mistake 2 — Narrowing Access When Overriding
1class Base {
2 public void display() { System.out.println("Base"); }
3}
4
5class Child extends Base {
6 // @Override
7 // private void display() { } // compile error — cannot reduce public to private
8
9 @Override
10 protected void display() { } // also error — protected < public
11
12 @Override
13 public void display() { System.out.println("Child"); } // correct
14}Mistake 3 — Assuming Sub-packages Share package-private Access
1// package com.myapp.util — class Helper with package-private method
2package com.myapp.util;
3class Helper {
4 void assist() { System.out.println("helping"); }
5}
6
7// package com.myapp.util.io — a SUB-package
8package com.myapp.util.io;
9import com.myapp.util.Helper; // compile error — Helper is package-private
10
11// Sub-packages are completely separate packages
12// package-private visibility does NOT cross sub-package boundariesMistake 4 — Using default Access Unintentionally
1// Forgot the modifier — this is DEFAULT (package-private), not public!
2class PaymentService { // ← no 'public' — package-private class
3 void processPayment() { } // ← no modifier — package-private method
4}
5
6// From another package:
7import com.myapp.PaymentService; // compile error — cannot access package-private class
8
9// Fix: add 'public' if it is meant to be part of the API
10public class PaymentService {
11 public void processPayment() { }
12}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.