Java Access Modifiers
Java Access Modifiers
Access modifiers control who can see and use a class, field, method, or constructor. They are the enforcement mechanism for encapsulation — the OOP principle that says objects should control what data they expose and what stays internal.
Java has four access levels. Three are keywords: private, protected, and public. The fourth has no keyword — it is the default level that applies when you write nothing, often called package-private. Choosing the right access level is not cosmetic — it determines what code can change, who can extend what, and how tightly coupled your classes become over time.
The Four Access Modifiers at a Glance
| Modifier | Keyword | Accessible From |
|---|---|---|
| Private | private | Same class only |
| Default (Package-Private) | (none) | Same class + same package |
| Protected | protected | Same class + same package + subclasses (any package) |
| Public | public | Everywhere — any class, any package |
This is the visibility ladder — each level adds more access than the one above it. private is the most restrictive; public is the most open.
What Can Access Modifiers Be Applied To?
Access modifiers can be applied to four Java constructs, but not all modifiers are valid on all constructs.
| Construct | private | default | protected | public |
|---|---|---|---|---|
| Top-level class | No | Yes | No | Yes |
| Nested class | Yes | Yes | Yes | Yes |
| Field | Yes | Yes | Yes | Yes |
| Method | Yes | Yes | Yes | Yes |
| Constructor | Yes | Yes | Yes | Yes |
A top-level class can only be public or package-private (default). Making a top-level class private or protected is a compile-time error — it makes no logical sense since there would be no way to access it.
private — Maximum Encapsulation
The private modifier restricts access to within the declaring class only. No other class — not even a subclass or a class in the same package — can access a private member.
Private is the default choice for all fields in production Java. The field is protected, and access is controlled through public methods.
1// File: BankAccount.java
2
3public class BankAccount {
4
5 // Private fields — no class outside BankAccount can read or write these directly
6 private final String accountNumber;
7 private double balance;
8 private int failedLoginAttempts;
9
10 public BankAccount(String accountNumber, double initialDeposit) {
11 this.accountNumber = accountNumber;
12 this.balance = Math.max(0.0, initialDeposit);
13 this.failedLoginAttempts = 0;
14 }
15
16 // Controlled read access
17 public double getBalance() {
18 return balance;
19 }
20
21 public String getAccountNumber() {
22 return accountNumber;
23 }
24
25 // Controlled write access with business rule
26 public boolean deposit(double amount) {
27 if (amount <= 0) return false;
28 balance += amount;
29 return true;
30 }
31
32 public boolean withdraw(double amount) {
33 if (amount <= 0 || amount > balance) return false;
34 balance -= amount;
35 return true;
36 }
37
38 // Private helper — internal logic, not exposed to callers
39 private boolean isBalanceSufficient(double amount) {
40 return balance >= amount;
41 }
42}1// File: PrivateDemo.java
2
3public class PrivateDemo {
4
5 public static void main(String[] args) {
6
7 BankAccount account = new BankAccount("ACC-2024-001", 10000.0);
8
9 System.out.println("Balance: Rs." + account.getBalance());
10 account.deposit(5000.0);
11 System.out.println("After deposit: Rs." + account.getBalance());
12
13 boolean success = account.withdraw(3000.0);
14 System.out.println("Withdrawal Rs.3000 succeeded: " + success);
15 System.out.println("Final balance: Rs." + account.getBalance());
16
17 // account.balance = 999999; // compile error — private field
18 // account.isBalanceSufficient(100); // compile error — private method
19 }
20}Output:
Balance: Rs.10000.0
After deposit: Rs.15000.0
Withdrawal Rs.3000 succeeded: true
Final balance: Rs.12000.0
The balance field is completely protected. External code cannot set it to an arbitrary value — every change goes through deposit() or withdraw(), which enforce business rules. The isBalanceSufficient() helper is also private — it is an implementation detail that the caller has no reason to know about.
default (Package-Private) — Collaboration Within a Package
When no modifier is written, the member has default access — also called package-private. It is accessible from any class in the same package but invisible to classes in other packages, including subclasses in other packages.
Default access is the right choice for classes and members that are implementation details of a package but need to collaborate with other classes in that package.
1// File: com/devstackflow/payment/PaymentValidator.java
2
3package com.devstackflow.payment;
4
5// Default class — accessible only within com.devstackflow.payment
6class PaymentValidator {
7
8 // Default method — visible to other classes in the same package
9 boolean isValidAmount(double amount) {
10 return amount > 0 && amount <= 500000.0;
11 }
12
13 boolean isValidUpiId(String upiId) {
14 return upiId != null && upiId.contains("@");
15 }
16}1// File: com/devstackflow/payment/PaymentService.java
2
3package com.devstackflow.payment;
4
5public class PaymentService {
6
7 // PaymentValidator is default — accessible because same package
8 private final PaymentValidator validator = new PaymentValidator();
9
10 public String processPayment(String upiId, double amount) {
11 if (!validator.isValidUpiId(upiId)) {
12 return "FAILED: Invalid UPI ID — " + upiId;
13 }
14 if (!validator.isValidAmount(amount)) {
15 return "FAILED: Invalid amount — Rs." + amount;
16 }
17 return "SUCCESS: Rs." + amount + " sent to " + upiId;
18 }
19}1// File: PaymentDemo.java
2
3import com.devstackflow.payment.PaymentService;
4
5public class PaymentDemo {
6
7 public static void main(String[] args) {
8
9 PaymentService service = new PaymentService();
10
11 System.out.println(service.processPayment("rahul@upi", 1500.0));
12 System.out.println(service.processPayment("invalid-id", 1500.0));
13 System.out.println(service.processPayment("priya@upi", -200.0));
14
15 // PaymentValidator validator = new PaymentValidator(); // compile error — outside package
16 }
17}Output:
SUCCESS: Rs.1500.0 sent to rahul@upi
FAILED: Invalid UPI ID — invalid-id
FAILED: Invalid amount — Rs.-200.0
PaymentValidator is an internal detail of the com.devstackflow.payment package. No code outside the package can see or instantiate it. PaymentService — which is public — is the only entry point. This is the standard pattern for package-level encapsulation in real Java projects.
protected — Inheritance-Aware Access
The protected modifier allows access from the same class, the same package, and any subclass — regardless of which package the subclass lives in. It is designed for inheritance scenarios where a parent class needs to share implementation details with child classes without exposing them to the entire world.
1// File: com/devstackflow/notification/BaseNotification.java
2
3package com.devstackflow.notification;
4
5public abstract class BaseNotification {
6
7 private final String notificationId;
8 private final String recipientId;
9
10 // Protected — subclasses can read this but external code cannot
11 protected final String message;
12 protected final int priority;
13
14 public BaseNotification(String notificationId, String recipientId,
15 String message, int priority) {
16 this.notificationId = notificationId;
17 this.recipientId = recipientId;
18 this.message = message;
19 this.priority = priority;
20 }
21
22 public String getNotificationId() { return notificationId; }
23 public String getRecipientId() { return recipientId; }
24
25 // Protected method — shared implementation detail for subclasses
26 protected String buildBasePayload() {
27 return "[P" + priority + "] To: " + recipientId + " | " + message;
28 }
29
30 // Abstract — each subclass provides its own delivery
31 public abstract void deliver();
32}1// File: com/devstackflow/sms/SmsNotification.java
2// Note: different package from BaseNotification
3
4package com.devstackflow.sms;
5
6import com.devstackflow.notification.BaseNotification;
7
8public class SmsNotification extends BaseNotification {
9
10 private final String phoneNumber;
11
12 public SmsNotification(String notificationId, String recipientId,
13 String phoneNumber, String message) {
14 super(notificationId, recipientId, message, 2);
15 this.phoneNumber = phoneNumber;
16 }
17
18 @Override
19 public void deliver() {
20 // Subclass can access protected fields and methods from parent
21 String payload = buildBasePayload(); // protected method — accessible in subclass
22 System.out.println("SMS to " + phoneNumber + ": " + payload);
23
24 // Accessing protected field directly
25 if (priority == 1) {
26 System.out.println(" [URGENT] Immediate delivery triggered.");
27 }
28 }
29}1// File: ProtectedDemo.java
2
3import com.devstackflow.sms.SmsNotification;
4
5public class ProtectedDemo {
6
7 public static void main(String[] args) {
8
9 SmsNotification sms = new SmsNotification(
10 "NOTIF-001", "USER-9876",
11 "9876543210", "Your OTP is 847291. Valid for 5 minutes."
12 );
13
14 sms.deliver();
15
16 // sms.message // compile error — protected, not a subclass here
17 // sms.buildBasePayload() // compile error — protected, not a subclass here
18 }
19}Output:
SMS to 9876543210: [P2] To: USER-9876 | Your OTP is 847291. Valid for 5 minutes.
SmsNotification is in a completely different package from BaseNotification, yet it can access message, priority, and buildBasePayload() because it is a subclass. ProtectedDemo — which is not a subclass — cannot access these members even though it is in the calling code.
public — Unrestricted Access
The public modifier makes a member accessible from any class in any package. Public is the right choice for APIs — the interface your class presents to the outside world. Everything that is not part of the public API should be something more restrictive.
1// File: PricingService.java
2
3public class PricingService {
4
5 // Public constants — part of the API, callers are allowed to read these
6 public static final double GST_RATE = 0.18;
7 public static final double FREE_DELIVERY_MINIMUM = 299.0;
8
9 // Public method — the API callers use
10 public double calculateTotal(double itemTotal, boolean includeDelivery) {
11 double gst = itemTotal * GST_RATE;
12 double deliveryFee = resolveDeliveryFee(itemTotal, includeDelivery);
13 return itemTotal + gst + deliveryFee;
14 }
15
16 // Private — internal detail, not part of the public API
17 private double resolveDeliveryFee(double itemTotal, boolean includeDelivery) {
18 if (!includeDelivery) return 0.0;
19 return itemTotal >= FREE_DELIVERY_MINIMUM ? 0.0 : 40.0;
20 }
21}1// File: PublicDemo.java
2
3public class PublicDemo {
4
5 public static void main(String[] args) {
6
7 PricingService pricing = new PricingService();
8
9 double total1 = pricing.calculateTotal(250.0, true);
10 double total2 = pricing.calculateTotal(350.0, true);
11
12 System.out.printf("Order Rs.250 total (with delivery): Rs.%.2f%n", total1);
13 System.out.printf("Order Rs.350 total (free delivery): Rs.%.2f%n", total2);
14 System.out.println("GST rate: " + (PricingService.GST_RATE * 100) + "%");
15 }
16}Output:
Order Rs.250 total (with delivery): Rs.335.00
Order Rs.350 total (free delivery): Rs.413.00
GST rate: 18.0%
calculateTotal() is public — callers use it. resolveDeliveryFee() is private — it is a helper that the caller has no business knowing about. If the delivery fee structure changes internally, no external code breaks because the caller only depends on calculateTotal().
Access Modifiers — Detailed Visibility Table
The table below shows exactly which contexts can access each modifier level. Use this as a quick reference.
| Access Level | Same Class | Same Package (non-subclass) | Subclass (different package) | Any Other Class |
|---|---|---|---|---|
private | Yes | No | No | No |
| default | Yes | Yes | No | No |
protected | Yes | Yes | Yes | No |
public | Yes | Yes | Yes | Yes |
private vs default vs protected vs public
| Aspect | private | default | protected | public |
|---|---|---|---|---|
| Keyword | private | (none) | protected | public |
| Same class | Yes | Yes | Yes | Yes |
| Same package | No | Yes | Yes | Yes |
| Subclass (other package) | No | No | Yes | Yes |
| Unrelated class (other package) | No | No | No | Yes |
| Typical use | Fields, helpers | Package internals | Inherited behaviour | Public API |
| Encapsulation strength | Strongest | Strong | Moderate | None |
When to Use Each Access Modifier
| Situation | Recommended Modifier | Reason |
|---|---|---|
| Class fields | private | All field access goes through methods — encapsulation |
| Public API methods | public | Callers outside the class need to use them |
| Internal helper methods | private | No caller outside the class needs them |
| Package-level implementation classes | default | Visible within package, hidden from outside |
| Methods shared with subclasses | protected | Inheritance needs access, world does not |
| Constants meant for all callers | public static final | API-level shared constant |
| Constructors for singletons | private | Prevents direct instantiation |
| Template method steps in abstract class | protected | Subclasses override them, callers do not call them |
Real-World Example — Logistics Tracking System
The Business Problem
You are building the shipment tracking backend for a logistics company — similar to what Delhivery or Ekart runs for its package movement system. The system has a base Shipment class with internal tracking state, a PriorityShipment subclass that can access certain parent internals for priority processing, and a TrackingService that the outside world uses through a public API. Each layer must see exactly what it needs and no more.
Implementation
1// File: com/devstackflow/logistics/Shipment.java
2
3package com.devstackflow.logistics;
4
5import java.time.LocalDateTime;
6import java.time.format.DateTimeFormatter;
7
8public class Shipment {
9
10 // Private — internal identity, no caller needs to set this
11 private final String shipmentId;
12
13 // Private — tracking history is internal state
14 private String currentStatus;
15 private String currentLocation;
16
17 // Protected — subclasses need to read these for priority handling
18 protected final String destinationCity;
19 protected final double weightKg;
20 protected int deliveryAttempts;
21
22 // Default — accessible within the logistics package for internal services
23 LocalDateTime estimatedDelivery;
24
25 public Shipment(String shipmentId, String destinationCity, double weightKg) {
26 this.shipmentId = shipmentId;
27 this.destinationCity = destinationCity;
28 this.weightKg = weightKg;
29 this.currentStatus = "BOOKED";
30 this.currentLocation = "Origin Hub";
31 this.deliveryAttempts = 0;
32 this.estimatedDelivery = LocalDateTime.now().plusDays(3);
33 }
34
35 // Public — callers use this to track status
36 public String getShipmentId() { return shipmentId; }
37 public String getCurrentStatus() { return currentStatus; }
38 public String getCurrentLocation(){ return currentLocation; }
39
40 // Protected — subclasses can update status during processing
41 protected void updateStatus(String status, String location) {
42 this.currentStatus = status;
43 this.currentLocation = location;
44 }
45
46 // Private helper — internal only
47 private String formatDateTime(LocalDateTime dt) {
48 return dt.format(DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm"));
49 }
50
51 // Public — callers use this for the tracking summary
52 public String getTrackingSummary() {
53 return shipmentId + " | " + currentStatus
54 + " | Location: " + currentLocation
55 + " | ETA: " + formatDateTime(estimatedDelivery);
56 }
57}1// File: com/devstackflow/logistics/PriorityShipment.java
2
3package com.devstackflow.logistics;
4
5public class PriorityShipment extends Shipment {
6
7 private final String priorityLevel;
8 private final String dedicatedCourierId;
9
10 public PriorityShipment(String shipmentId, String destinationCity,
11 double weightKg, String priorityLevel,
12 String dedicatedCourierId) {
13 super(shipmentId, destinationCity, weightKg);
14 this.priorityLevel = priorityLevel;
15 this.dedicatedCourierId = dedicatedCourierId;
16 // Override default ETA for priority shipments — accesses package-default field
17 this.estimatedDelivery = java.time.LocalDateTime.now().plusHours(24);
18 }
19
20 public void attemptDelivery(String hubLocation) {
21 deliveryAttempts++; // protected field — accessible in subclass
22 if (deliveryAttempts <= 3) {
23 // Protected method — subclass calls parent's protected method
24 updateStatus("OUT_FOR_DELIVERY", hubLocation);
25 System.out.println("[" + priorityLevel + "] Attempt "
26 + deliveryAttempts + " from " + hubLocation
27 + " by courier " + dedicatedCourierId);
28 } else {
29 updateStatus("DELIVERY_FAILED", hubLocation);
30 System.out.println("Delivery failed after " + deliveryAttempts + " attempts.");
31 }
32 }
33
34 public boolean isHeavyShipment() {
35 return weightKg > 10.0; // protected field — directly accessible
36 }
37}1// File: TrackingDemo.java
2
3import com.devstackflow.logistics.PriorityShipment;
4import com.devstackflow.logistics.Shipment;
5
6public class TrackingDemo {
7
8 public static void main(String[] args) {
9
10 Shipment standard = new Shipment("SHP-001", "Chennai", 2.5);
11 PriorityShipment priority = new PriorityShipment(
12 "SHP-002", "Mumbai", 15.0, "EXPRESS", "DRV-4521"
13 );
14
15 System.out.println("=== Tracking Report ===\n");
16 System.out.println(standard.getTrackingSummary());
17 System.out.println(priority.getTrackingSummary());
18
19 System.out.println("\n--- Priority Delivery Attempts ---");
20 priority.attemptDelivery("Andheri Hub");
21 System.out.println(priority.getTrackingSummary());
22 System.out.println("Heavy shipment: " + priority.isHeavyShipment());
23
24 // priority.weightKg // compile error — protected, not a subclass here
25 // priority.estimatedDelivery // compile error — default, different package
26 // priority.updateStatus(...) // compile error — protected, not a subclass here
27 }
28}Output:
=== Tracking Report ===
SHP-001 | BOOKED | Location: Origin Hub | ETA: 18-01-2024 10:30
SHP-002 | BOOKED | Location: Origin Hub | ETA: 16-01-2024 10:30
--- Priority Delivery Attempts ---
[EXPRESS] Attempt 1 from Andheri Hub by courier DRV-4521
SHP-002 | OUT_FOR_DELIVERY | Location: Andheri Hub | ETA: 16-01-2024 10:30
Heavy shipment: true
Each access level does exactly what it should. private fields like shipmentId are completely hidden. protected fields like weightKg and deliveryAttempts are accessible to PriorityShipment for its processing logic. The default field estimatedDelivery is accessible within the logistics package for internal coordination. The public API — getTrackingSummary(), getShipmentId() — is what external callers use.
Best Practices
Start with private — open up only when there is a reason
The discipline is to make everything private first, then consciously decide to widen access only when a genuine need exists. Opening access is easy; closing it after other code depends on it is painful. Teams following clean architecture treat any non-private field as a design decision that requires justification in a code review.
Never make fields public
A public field gives any code in any package the ability to set it to any value without going through any validation. If the field needs to be readable, add a getter. If it needs to be writable, add a setter with validation. A mistake that appears often in fresher pull requests is making fields public to avoid writing getters — this trades a few lines of code for a complete loss of encapsulation.
Use protected deliberately — not as a lazy alternative to private
protected exists specifically for inheritance. If you are making a member protected for a reason other than "subclasses need it," reconsider. Protected members become part of your class's contract with subclasses — changing or removing them later breaks every subclass, even ones written by other teams.
Prefer package-private for internal collaborators
When two classes in the same package need to work together closely but the collaboration should not be visible to the outside world, default access is the right tool. It is more restrictive than protected but allows package-level collaboration.
Common Mistakes
Mistake 1 — Making Fields public
1// Wrong — any code anywhere can corrupt this data
2public class UserProfile {
3 public String username; // no validation possible
4 public int age; // can be set to -500
5 public String email; // can be set to anything
6}
7
8// Correct — fields are private, access is controlled
9public class UserProfile {
10 private String username;
11 private int age;
12 private String email;
13
14 public void setAge(int age) {
15 if (age < 0 || age > 120) throw new IllegalArgumentException("Invalid age: " + age);
16 this.age = age;
17 }
18}Mistake 2 — Confusing protected with package-private
1// Developer expects protected to behave like private outside the package
2// But a subclass in ANY package can access protected members
3
4package com.other.package;
5
6import com.devstackflow.logistics.Shipment;
7
8public class RogueSubclass extends Shipment {
9
10 public RogueSubclass() {
11 super("HACK-001", "Unknown", 0.0);
12 }
13
14 public void expose() {
15 // This compiles and runs — protected members are visible to ALL subclasses
16 System.out.println("Weight: " + weightKg); // protected field — accessible
17 updateStatus("TAMPERED", "Unknown"); // protected method — accessible
18 }
19}protected does not mean "accessible only to subclasses in my package." It means "accessible to all subclasses everywhere." Any developer who extends your class gains access to all its protected members. During code reviews, seniors commonly flag unnecessary protected on members that should have been private.
Mistake 3 — Using public for Everything
1public class OrderProcessor {
2
3 public double taxRate = 0.18; // should be private static final
4 public String lastOrderId = ""; // should be private
5
6 public double calculateTax(double amount) { return amount * taxRate; }
7 public void logOrder(String id) { this.lastOrderId = id; }
8 public boolean validateAmount(double amt) { return amt > 0; } // internal helper
9}Every method and field being public means any code anywhere depends on all of them. Changing taxRate from double to BigDecimal for precision, or removing logOrder() because it was replaced with a logging framework, now breaks callers you did not know existed. Minimal public API means fewer unintended dependencies.
Mistake 4 — Trying to Apply private or protected to a Top-Level Class
1// Compile error — top-level classes can only be public or default
2private class InternalService { } // error
3protected class SharedHelper { } // error
4
5// Correct options for top-level classes
6public class InternalService { } // visible everywhere
7class SharedHelper { } // visible within the package onlyOnly nested classes can use private or protected. Top-level classes have only two options: public or package-private (default). If you need a class to be private to its enclosing class, make it a nested class.
Interview Questions
Q1. What are the four access modifiers in Java and what is their visibility?
Java has private (same class only), default/package-private (same class and same package), protected (same class, same package, and any subclass in any package), and public (everywhere). They form a visibility ladder — each level adds access on top of the one before it. private is the most restrictive and public is the most open. The default level has no keyword — writing nothing applies it.
Q2. What is the difference between protected and default (package-private) access?
Both allow access within the same class and same package. The difference is subclass access: protected members are visible to subclasses in any package, while default members are not accessible to subclasses outside the package. Use protected when inheritance is the reason for sharing — for template methods and hook points that subclasses need to override or call. Use default when the sharing is purely a package-level implementation concern with no inheritance relationship involved.
Q3. Can a private member of a class be accessed by its subclass?
No. Private members are accessible only within the declaring class itself. Subclasses do not inherit private members — they exist in the parent's objects but the subclass code cannot reference them directly. If a subclass needs access to parent state, the parent must expose it through a protected or public method. This is the correct design — the parent controls what the subclass can see.
Q4. Why should fields almost always be private in Java?
A public field allows any code anywhere to read or write the field value without going through any validation or control logic. A private field forces all access through methods, which can enforce business rules, trigger side effects, and maintain invariants. If the internal storage representation changes — from int to long, or from an array to a List — a private field with public methods allows that change without breaking callers. A public field embeds the implementation detail into every caller.
Q5. What is the difference between private and protected for a constructor?
A private constructor prevents any class — including subclasses — from instantiating the class directly. It is used for singleton patterns and utility classes. A protected constructor allows subclasses to call it using super() but prevents direct instantiation from unrelated classes. Abstract classes often use protected constructors — they must be subclassed to be used, and the constructor is there for subclass initialisation, not for direct external use.
Q6. What happens when you do not write any access modifier on a class member?
The member gets default (package-private) access. It is accessible from any class within the same package but invisible to classes in other packages, even subclasses. This is not public by accident — it is a deliberate, useful access level for package-internal collaboration. Teams often use it for classes and methods that support a public-facing class within the same package without being part of the external API.
FAQs
What are access modifiers in Java?
Access modifiers are keywords that control the visibility of a class, field, method, or constructor. Java has four: private (same class only), default/package-private (same package), protected (same package plus subclasses), and public (everywhere). They are the primary mechanism for implementing encapsulation.
What is package-private in Java?
Package-private is the access level that applies when no modifier keyword is written. A package-private member is visible to all classes in the same package but invisible to classes outside the package — including subclasses in other packages. It is not a keyword; the absence of a modifier keyword is what activates it.
Can a class be private in Java?
A top-level class cannot be private or protected — only public or default. A nested class (a class declared inside another class) can be private, which makes it visible only within the outer class. private nested classes are commonly used for implementation helpers that should not be exposed even at the package level.
What is the difference between private and protected in Java?
private restricts access to the declaring class only — subclasses cannot access it. protected allows access from the same class, same package, and any subclass anywhere. Use private for internal implementation details that no one — not even subclasses — should touch. Use protected when inheritance is the intentional design and subclasses need to work with the member.
Should all fields be private in Java?
Yes, in almost all cases. Making fields private is the foundation of encapsulation. The only common exceptions are public static final constants — values that are inherently public by nature. Even then, teams sometimes make constants package-private if they are only needed within a module.
What access modifier should I use for methods in an abstract class?
Methods in an abstract class that subclasses must override are typically public or protected abstract. Methods that provide shared implementation for subclasses are typically protected. Methods that are purely internal helpers are private. The abstract class's public API — what callers use — should be public. The template method pattern specifically uses protected for the hook methods that subclasses override.
Summary
Access modifiers enforce the visibility contract of every class, field, method, and constructor in Java. The four levels — private, default, protected, and public — form a ladder from most restrictive to most open. The discipline of starting with private and opening up only when there is a genuine reason is what keeps codebases maintainable and prevents unintended coupling between components.
The most important practical rule is that fields should almost always be private, with access going through methods that can validate, control, and evolve the implementation without breaking callers. protected exists for inheritance and nothing else. public is for the external API that callers genuinely need. Default is for package-level collaboration that belongs inside a module but not in its public interface.
For interviews, be ready to reproduce the four-level visibility table from memory, explain the specific difference between protected and default with a concrete example, and articulate why private fields with public methods is the correct encapsulation pattern. These are the most consistently tested points across service-based and product-based Java interviews.
What to Read Next
| Topic | Link |
|---|---|
| How encapsulation uses private fields and public methods to protect state | Encapsulation → |
| How inheritance determines which protected members a subclass can access | Inheritance → |
| How constructors use access modifiers for singleton and factory patterns | Constructors → |
| How interfaces define public contracts for abstraction | Interfaces → |
| How abstract classes use protected methods as template method hooks | Abstract Classes → |