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.

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

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

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 boundaries

Mistake 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: privatedefaultprotectedpublic. 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