Java protected Modifier
Java protected Modifier
protected sits exactly between default and public in Java's visibility spectrum. A protected member is accessible to all classes in the same package — just like default — plus one extra group: subclasses in any package, regardless of where they are.
This dual nature is what makes protected useful and also what makes it subtle. Same-package access is easy to understand. The cross-package subclass access has a specific rule: a subclass in a different package can access a protected member only through its own inheritance — not through an arbitrary reference to the parent type. That distinction matters and appears in interviews regularly.
The protected Access Rule — Exactly What It Permits
Who can access a protected member?
SAME PACKAGE:
Any class in the same package — identical to default access.
No inheritance required.
DIFFERENT PACKAGE — SUBCLASS ONLY:
A subclass CAN access protected members through:
— super.method() ← call parent's protected method
— super.field ← read parent's protected field
— this.field ← own inherited protected field
— new ChildInstance.field ← through a subclass-typed reference
A subclass CANNOT access protected members through:
— new ParentInstance.field ← arbitrary parent reference from diff package
— new SiblingChild.field ← sibling subclass reference from diff package
EVERYWHERE ELSE: not accessible (no package, no inheritance)
Quick memory rule:
private → this class only
default → this package
protected → this package + subclasses anywhere
public → everywhere
1 — protected Fields and Methods: Inheritance Hook
protected fields and methods are the tools for building extensible class hierarchies. The parent class provides a stable public API for external callers, and protected members that subclasses can use and override.
// File: com/devstackflow/base/Shape.java
package com.devstackflow.base;
public abstract class Shape {
// protected — subclasses in any package can read and write
protected String color;
protected boolean filled;
// protected constructor — subclasses call super(...)
protected Shape(String color, boolean filled) {
this.color = color;
this.filled = filled;
}
// protected method — subclasses can override this hook
protected String getShapeInfo() {
return color + (filled ? " filled" : " unfilled");
}
// abstract — subclasses must provide implementation
public abstract double area();
public abstract double perimeter();
// public final — concrete template method using protected hook
public final void printDetails() {
System.out.printf("Shape : %s%n", getClass().getSimpleName());
System.out.printf("Info : %s%n", getShapeInfo());
System.out.printf("Area : %.2f sq units%n", area());
System.out.printf("Perim : %.2f units%n%n", perimeter());
}
}// File: com/devstackflow/shapes/Circle.java
package com.devstackflow.shapes; // DIFFERENT package from Shape
import com.devstackflow.base.Shape;
public class Circle extends Shape {
private double radius;
public Circle(double radius, String color, boolean filled) {
super(color, filled); // protected constructor — accessible via super()
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
// Overrides protected method — adds circle-specific detail
@Override
protected String getShapeInfo() {
return super.getShapeInfo() + " | radius=" + radius; // super.getShapeInfo() OK
}
// Can access inherited protected fields directly
public void changeColor(String newColor) {
this.color = newColor; // protected field — accessible in subclass
}
}// File: com/devstackflow/shapes/Rectangle.java
package com.devstackflow.shapes;
import com.devstackflow.base.Shape;
public class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height, String color, boolean filled) {
super(color, filled); // protected constructor
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
@Override
protected String getShapeInfo() {
return super.getShapeInfo() + " | " + width + "x" + height;
}
}// File: com/devstackflow/demo/ShapeDemo.java
package com.devstackflow.demo;
import com.devstackflow.shapes.Circle;
import com.devstackflow.shapes.Rectangle;
import com.devstackflow.base.Shape;
public class ShapeDemo {
public static void main(String[] args) {
Circle circle = new Circle(7.0, "Red", true);
Rectangle rect = new Rectangle(5.0, 3.0, "Blue", false);
circle.printDetails(); // public — accessible
rect.printDetails(); // public — accessible
circle.changeColor("Green");
System.out.println("Changed circle color to Green.");
circle.printDetails();
// From outside the package hierarchy — protected is NOT accessible:
// Shape s = new Circle(5, "Black", false);
// s.color = "White"; ← compile error — protected via non-subclass ref
// s.getShapeInfo(); ← compile error — protected via non-subclass ref
}
}Output:
Shape : Circle
Info : Red filled | radius=7.0
Area : 153.94 sq units
Perim : 43.98 units
Shape : Rectangle
Info : Blue unfilled | 5.0x3.0
Area : 15.00 sq units
Perim : 16.00 units
Changed circle color to Green.
Shape : Circle
Info : Green filled | radius=7.0
Area : 153.94 sq units
Perim : 43.98 units
Circle and Rectangle live in a different package from Shape. They can access color, filled, and getShapeInfo() through inheritance — through this, super, or as their own fields. ShapeDemo in com.devstackflow.demo cannot access those protected members through a Shape reference because ShapeDemo is not a subclass of Shape.
2 — protected Constructor: Subclass-Only Creation
A protected constructor allows subclasses (in any package) to call super(...) while preventing direct instantiation with new ParentClass() from non-subclass code outside the package.
// File: com/devstackflow/event/DomainEvent.java
package com.devstackflow.event;
import java.time.LocalDateTime;
import java.util.UUID;
public abstract class DomainEvent {
private final String eventId;
private final LocalDateTime occurredAt;
protected final String aggregateId; // subclasses need it
protected final String eventType; // subclasses set it
// protected constructor — only subclasses can call super()
// Not abstract — has body — just restricted creation
protected DomainEvent(String aggregateId, String eventType) {
this.eventId = UUID.randomUUID().toString();
this.occurredAt = LocalDateTime.now();
this.aggregateId = aggregateId;
this.eventType = eventType;
}
// public — everyone can read these
public String getEventId() { return eventId; }
public LocalDateTime getOccurredAt() { return occurredAt; }
public String getAggregateId() { return aggregateId; }
public String getEventType() { return eventType; }
// protected — subclasses can override to add detail
protected String getEventDetail() {
return "aggregateId=" + aggregateId;
}
@Override
public String toString() {
return String.format("[%s] %s | %s | %s",
eventType, eventId.substring(0, 8),
occurredAt.toLocalTime().withNano(0),
getEventDetail());
}
}// File: com/devstackflow/order/events/OrderPlacedEvent.java
package com.devstackflow.order.events; // different package from DomainEvent
import com.devstackflow.event.DomainEvent;
public class OrderPlacedEvent extends DomainEvent {
private final String customerId;
private final double amount;
public OrderPlacedEvent(String orderId, String customerId, double amount) {
super(orderId, "ORDER_PLACED"); // protected constructor — via super()
this.customerId = customerId;
this.amount = amount;
}
@Override
protected String getEventDetail() {
// Can reference protected field aggregateId
return "orderId=" + aggregateId
+ " | customer=" + customerId
+ " | amount=Rs." + amount;
}
}// File: com/devstackflow/order/events/OrderCancelledEvent.java
package com.devstackflow.order.events;
import com.devstackflow.event.DomainEvent;
public class OrderCancelledEvent extends DomainEvent {
private final String reason;
public OrderCancelledEvent(String orderId, String reason) {
super(orderId, "ORDER_CANCELLED");
this.reason = reason;
}
@Override
protected String getEventDetail() {
return "orderId=" + aggregateId + " | reason=" + reason;
}
}// File: com/devstackflow/demo/EventDemo.java
package com.devstackflow.demo;
import com.devstackflow.order.events.OrderPlacedEvent;
import com.devstackflow.order.events.OrderCancelledEvent;
import com.devstackflow.event.DomainEvent;
public class EventDemo {
public static void main(String[] args) {
System.out.println("=== Domain Events ===\n");
OrderPlacedEvent placed = new OrderPlacedEvent(
"ORD-001", "CUST-101", 2499.0);
OrderCancelledEvent cancelled = new OrderCancelledEvent(
"ORD-002", "Customer requested");
System.out.println(placed);
System.out.println(cancelled);
System.out.println("\n=== Public fields from outside ===");
System.out.println("Event ID : " + placed.getEventId());
System.out.println("Aggregate : " + placed.getAggregateId());
System.out.println("Event Type : " + placed.getEventType());
// Cannot do from non-subclass in different package:
// new DomainEvent("x", "y"); ← compile error — protected constructor
// placed.aggregateId; ← compile error — protected field via non-subclass
// placed.getEventDetail(); ← compile error — protected method
}
}Output:
=== Domain Events ===
[ORDER_PLACED] a1b2c3d4 | 10:30:00 | orderId=ORD-001 | customer=CUST-101 | amount=Rs.2499.0
[ORDER_CANCELLED] e5f6g7h8 | 10:30:00 | orderId=ORD-002 | reason=Customer requested
=== Public fields from outside ===
Event ID : a1b2c3d4-...full UUID...
Aggregate : ORD-001
Event Type : ORDER_PLACED
DomainEvent cannot be instantiated directly — protected constructor means only subclasses can call super(). EventDemo uses OrderPlacedEvent and OrderCancelledEvent through their public constructors and reads through public getters. The protected fields aggregateId and eventType are accessible inside the event subclasses but not from EventDemo.
3 — The Critical Rule: Qualified Access in Subclasses
The most misunderstood aspect of protected: in a different package, a subclass can access a protected member only through its own type — not through a reference to the parent type or a sibling subclass.
// File: com/devstackflow/base/Account.java
package com.devstackflow.base;
public class Account {
protected double balance = 0.0; // protected
protected void credit(double amount) {
balance += amount;
}
}// File: com/devstackflow/banking/SavingsAccount.java
package com.devstackflow.banking; // different package
import com.devstackflow.base.Account;
public class SavingsAccount extends Account {
public void deposit(double amount) {
// OK — accessing own inherited protected member (through 'this')
credit(amount); // own inherited protected method
System.out.println("Balance after deposit: " + balance); // own protected field
}
public void transfer(SavingsAccount other, double amount) {
// OK — 'other' is the SAME subclass type (SavingsAccount)
// Java 11+ allows this; protected via subclass-typed reference
other.credit(amount); // OK — other is SavingsAccount
System.out.println("Transferred Rs." + amount);
}
public void illegal(Account genericAccount) {
// COMPILE ERROR — accessing protected via a plain Account reference
// from a different package is NOT allowed
// genericAccount.credit(100); ← error
// genericAccount.balance; ← error
// The reference type is Account, not SavingsAccount
System.out.println("Cannot access protected via Account ref from diff package");
}
}// File: com/devstackflow/banking/AccessRuleDemo.java
package com.devstackflow.banking;
import com.devstackflow.base.Account;
public class AccessRuleDemo {
public static void main(String[] args) {
SavingsAccount acc1 = new SavingsAccount();
SavingsAccount acc2 = new SavingsAccount();
acc1.deposit(5000);
acc1.transfer(acc2, 2000);
System.out.println("acc1 balance: " + acc1.balance); // OK — SavingsAccount ref
System.out.println("acc2 balance: " + acc2.balance); // OK — SavingsAccount ref
// From non-subclass code in same package — still blocked:
Account base = new Account();
// base.balance; ← compile error even in same package as SavingsAccount
// because Account is in a different package
// BUT: from inside com.devstackflow.base (same package as Account):
// base.balance; would be OK — same package
}
}Output:
Balance after deposit: 5000.0
Transferred Rs.2000.0
acc1 balance: 3000.0
acc2 balance: 2000.0
protected vs private vs default vs public — Quick Comparison Table
| Aspect | private | default | protected | public |
|---|---|---|---|---|
| Same class | ✓ | ✓ | ✓ | ✓ |
| Same package, diff class | ✗ | ✓ | ✓ | ✓ |
| Subclass, same package | ✗ | ✓ | ✓ | ✓ |
| Subclass, different package | ✗ | ✗ | ✓ | ✓ |
| Non-subclass, different package | ✗ | ✗ | ✗ | ✓ |
| Can override when inherited | N/A (not inherited) | ✓ (same package subclass) | ✓ (any subclass) | ✓ |
| Valid on top-level class | ✗ | ✓ | ✗ | ✓ |
| Valid on member | ✓ | ✓ | ✓ | ✓ |
| Main use case | Encapsulation | Package-level helpers | Extensibility hooks | Public API |
| Keyword | private | (none) | protected | public |
4 — protected and the Template Method Pattern
protected is the backbone of the Template Method design pattern — one of the most common patterns in Java frameworks. The parent defines the algorithm structure as public final, and protected hooks that subclasses customise.
// File: com/devstackflow/report/ReportGenerator.java
package com.devstackflow.report;
public abstract class ReportGenerator {
// public final — the algorithm structure; subclasses cannot change the order
public final String generate(String title) {
StringBuilder sb = new StringBuilder();
sb.append(buildHeader(title)); // protected hook
sb.append(buildBody()); // protected hook — must implement
sb.append(buildFooter()); // protected hook
return sb.toString();
}
// protected — default header; subclasses may override
protected String buildHeader(String title) {
return "=".repeat(50) + "\n" + " " + title.toUpperCase() + "\n"
+ "=".repeat(50) + "\n";
}
// protected abstract — subclasses MUST provide the body
protected abstract String buildBody();
// protected — default footer; subclasses may override
protected String buildFooter() {
return "-".repeat(50) + "\nGenerated: "
+ java.time.LocalDate.now() + "\n";
}
}// File: com/devstackflow/report/SalesReport.java
package com.devstackflow.report;
import java.util.Map;
public class SalesReport extends ReportGenerator {
private final Map<String, Double> salesData;
public SalesReport(Map<String, Double> salesData) {
this.salesData = salesData;
}
@Override
protected String buildBody() {
StringBuilder body = new StringBuilder();
body.append(String.format("%-20s %12s%n", "Product", "Revenue"));
body.append("-".repeat(34)).append("\n");
double total = 0;
for (Map.Entry<String, Double> e : salesData.entrySet()) {
body.append(String.format("%-20s Rs.%9.2f%n", e.getKey(), e.getValue()));
total += e.getValue();
}
body.append("-".repeat(34)).append("\n");
body.append(String.format("%-20s Rs.%9.2f%n", "TOTAL", total));
return body.toString();
}
@Override
protected String buildFooter() {
return super.buildFooter() + "Confidential — Finance Team Only\n";
}
}// File: com/devstackflow/report/AttendanceReport.java
package com.devstackflow.report;
import java.util.Map;
public class AttendanceReport extends ReportGenerator {
private final Map<String, Integer> attendanceData;
private final int totalDays;
public AttendanceReport(Map<String, Integer> attendanceData, int totalDays) {
this.attendanceData = attendanceData;
this.totalDays = totalDays;
}
@Override
protected String buildBody() {
StringBuilder body = new StringBuilder();
body.append(String.format("%-20s %8s %8s%n", "Employee", "Days", "%"));
body.append("-".repeat(38)).append("\n");
attendanceData.forEach((name, days) -> {
double pct = (days * 100.0) / totalDays;
String flag = pct < 75 ? " ← LOW" : "";
body.append(String.format("%-20s %8d %7.1f%%%s%n",
name, days, pct, flag));
});
return body.toString();
}
}// File: com/devstackflow/demo/ReportDemo.java
package com.devstackflow.demo;
import com.devstackflow.report.SalesReport;
import com.devstackflow.report.AttendanceReport;
import java.util.LinkedHashMap;
import java.util.Map;
public class ReportDemo {
public static void main(String[] args) {
// Sales Report
Map<String, Double> sales = new LinkedHashMap<>();
sales.put("Laptop", 450000.0);
sales.put("Headphones", 89000.0);
sales.put("Keyboard", 45000.0);
sales.put("Monitor", 180000.0);
SalesReport salesReport = new SalesReport(sales);
System.out.println(salesReport.generate("Monthly Sales Report — January 2024"));
// Attendance Report
Map<String, Integer> attendance = new LinkedHashMap<>();
attendance.put("Priya Sharma", 22);
attendance.put("Rohan Mehta", 19);
attendance.put("Sneha Rao", 16);
attendance.put("Karan Singh", 23);
AttendanceReport attReport = new AttendanceReport(attendance, 24);
System.out.println(attReport.generate("Employee Attendance — January 2024"));
}
}Output:
==================================================
MONTHLY SALES REPORT — JANUARY 2024
==================================================
Product Revenue
----------------------------------
Laptop Rs. 450000.00
Headphones Rs. 89000.00
Keyboard Rs. 45000.00
Monitor Rs. 180000.00
----------------------------------
TOTAL Rs. 764000.00
--------------------------------------------------
Generated: 2024-01-15
Confidential — Finance Team Only
==================================================
EMPLOYEE ATTENDANCE — JANUARY 2024
==================================================
Employee Days %
--------------------------------------
Priya Sharma 22 91.7%
Rohan Mehta 19 79.2%
Sneha Rao 16 66.7% ← LOW
Karan Singh 23 95.8%
--------------------------------------------------
Generated: 2024-01-15
SalesReport overrides buildBody() (required) and buildFooter() (optional — calls super.buildFooter() first). AttendanceReport only overrides buildBody() — it uses the default header and footer from the parent. The generate() method in the parent is public final — neither subclass can change the structure. The protected hooks give targeted flexibility without exposing the internals.
Basic Example
class Vehicle {
protected String type = "Generic"; // protected — visible in this package and to subclasses anywhere
protected void describe() { // protected method
System.out.println("Vehicle type: " + type);
}
}
// Imagine this class lives in a DIFFERENT package, e.g. package com.example.cars;
// It would still be able to access Vehicle's protected members through inheritance.
class Car extends Vehicle {
Car() {
this.type = "Car"; // accessing inherited protected field
}
void show() {
describe(); // calling inherited protected method
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.show();
}
}Output
Vehicle type: Car
- ›
typeanddescribe()are declaredprotectedinVehicle— visible to every class in the same package, plus any subclass in any package. - ›
Car extends Vehicle, so even ifCarlived in a different package (as the comment shows), it could still setthis.typeand calldescribe()because it inherits fromVehicle. - ›
Car's constructor sets the inheritedtypefield to"Car", andshow()calls the inheriteddescribe()method, which prints the updated value. - ›A non-subclass in a different package holding only a
Vehiclereference would not be able to accesstypeordescribe()—protectedaccess is granted through inheritance, not through an arbitrary parent-type reference.
Best Practices
Use protected for template method hooks, not for sharing state. Protected fields create tight coupling between parent and all subclasses — any change to the field name, type, or semantics must be coordinated across every subclass. Protected methods (especially abstract ones) are cleaner extension points. Prefer exposing a protected method that subclasses override rather than a protected field they read directly.
Always call super.protectedMethod() when overriding, unless you intentionally replace the entire behaviour. When a parent's protected method does work that is still relevant — like base validation, logging, or setup — calling super.method() first in the override preserves that work. Only skip super when the override is designed to completely replace the parent's logic.
Keep the number of protected members small. Every protected member is an implicit contract with every subclass. The more protected members a class has, the harder it becomes to change the parent without reviewing all subclasses. Favour fewer, well-named protected hooks over many protected implementation details.
Document protected members as part of the API for subclass authors. Unlike private (only you) and public (all callers), protected targets a specific audience — developers writing subclasses. Document what protected fields and methods are expected to contain, what invariants they must preserve, and what the expected override contract is.
Common Mistakes
Mistake 1 — Accessing protected via Parent-Type Reference from Different Package
// In com.devstackflow.banking (different package from Account)
import com.devstackflow.base.Account;
class AuditService {
void audit(Account account) {
// WRONG — accessing protected via Account reference from outside the package
System.out.println(account.balance); // compile error
account.credit(100); // compile error
}
}
// Fix — AuditService must be in com.devstackflow.base (same package)
// OR Account must make the method public if external callers need itMistake 2 — Forgetting to Call super() in Overriding Protected Method
class Logger extends BaseLogger {
@Override
protected void log(String message) {
// Forgot super.log(message) — parent's logging setup is skipped
System.out.println("[CHILD] " + message);
}
}
// If parent.log() did important work (timestamp, level, formatting):
class Logger extends BaseLogger {
@Override
protected void log(String message) {
super.log(message); // preserve parent's work
System.out.println("[CHILD] " + message); // then add own
}
}Mistake 3 — Using protected When private Is Sufficient
// WRONG — making a helper protected "just in case" a subclass needs it
public class OrderService {
protected String buildQuery(String filter) { // exposed to all subclasses
return "SELECT * FROM orders WHERE " + filter;
}
}
// CORRECT — if no subclass needs it, keep it private
public class OrderService {
private String buildQuery(String filter) { // sealed
return "SELECT * FROM orders WHERE " + filter;
}
}
// Only widen to protected when a specific subclass need is confirmedMistake 4 — Reducing protected Access When Overriding
class Parent {
protected void doWork() { System.out.println("Parent work"); }
}
class Child extends Parent {
// @Override
// private void doWork() { } // compile error — cannot reduce protected to private
@Override
protected void doWork() { System.out.println("Child work"); } // same — OK
// OR
// public void doWork() { } // widening to public — also OK
}Interview Questions
Q1. What does the protected access modifier provide in Java?
protected provides two layers of access. First, the same layer as default — all classes within the same package can access the member. Second, an additional layer — subclasses in any package can access the member through inheritance: through super, through this, or through a reference typed as the subclass. The member is not accessible to non-subclass code in a different package, even if that code holds a reference to the parent type.
Q2. What is the difference between protected and default (package-private)?
Both protected and default allow access within the same package. The sole difference is inheritance across packages: protected additionally allows subclasses in different packages to access the member, while default does not. A default method in package A is invisible to a subclass in package B. A protected method in package A is accessible to subclasses in package B through the subclass reference. For all intra-package access, they behave identically.
Q3. Can a subclass in a different package access a protected member through a parent-type reference?
No — this is the critical rule that catches most developers. In a different package, a subclass can access the protected member only through its own type (via this, super, or a reference typed as the subclass). Accessing it through a plain parent-type reference — Account account = ...; account.balance — from a different package is a compile error even inside a subclass. The restriction is: you must prove you are accessing it through the inheritance chain, not as an arbitrary external caller who happens to hold a parent reference.
Q4. Can protected be used on a top-level class?
No. Only public and default (no modifier) are valid on top-level classes. protected and private cause a compile error on top-level class declarations. protected is valid on inner classes, nested static classes, and all class members — fields, methods, and constructors.
Q5. How does protected support the Template Method design pattern?
In the Template Method pattern, the parent class declares the algorithm structure in a public final method that calls several protected hook methods. Subclasses override the protected hooks to supply their specific behaviour without changing the algorithm's structure. The protected modifier is essential because the hooks need to be accessible from subclasses in any package, but they should not be part of the public API — callers invoke the final template method, not the individual hooks.
Q6. What happens when you override a protected method in a subclass?
An overriding method must maintain or widen the visibility — never narrow it. A protected method can be overridden as protected (same) or public (wider). It cannot be overridden as default or private — the compiler rejects this because it would break the Liskov Substitution Principle: callers that could invoke the protected method through a parent reference (in the same package) would no longer be able to call the child version.
FAQs
Can a protected constructor be called from a non-subclass in the same package?
Yes. Within the same package, protected behaves exactly like default — any class in the package can call the protected constructor with new ParentClass(args). The cross-package restriction only applies to different packages, where only subclasses can call super(args). From within the declaring package, protected provides full package-level access.
Is protected access granted to an interface that extends another interface?
No. Interfaces only deal with public members. An interface extending another interface inherits its abstract methods, all of which are implicitly public. protected and default are not valid on interface abstract methods. protected is meaningful only in class hierarchies, not interface hierarchies.
Why do some frameworks mark fields protected instead of private?
Frameworks like Spring and Hibernate often extend classes reflectively or generate subclasses at runtime (proxies). protected fields are accessible from these generated subclasses, while private fields would require reflection with setAccessible(true). The tradeoff is that protected fields are less tightly encapsulated — anyone writing a subclass can access them directly. This is one of the reasons the Spring community recommends using the protected modifier on @Autowired fields instead of private when using constructor injection is not possible.
Can you have a protected field with no getter?
Yes — within the class and its subclasses, the field is accessed directly without a getter. This is common in abstract base classes where the protected field is expected to be read and written by subclasses as part of the implementation. It is generally better practice to expose even protected fields through protected getters and setters — this gives the parent class control over what happens when subclasses read or change the value.
Does protected affect the class loader or module system?
In the Java module system (Java 9+), protected still works as described, but only if the package is exports in the module-info.java. If a module does not export the package containing the protected member, no outside code — including subclasses in other modules — can access it, regardless of the protected modifier. The module system adds a layer of encapsulation above protected. Within a single unnamed module (the traditional classpath), protected behaves exactly as described throughout this article.
Summary
protected is the modifier for inheritance-based extensibility. It grants the same package-level access as default, plus one more group: subclasses in any package. Subclasses access protected members through inheritance — this, super, or a subclass-typed reference — never through an arbitrary parent-type reference from a different package.
Its primary use cases are template method hooks, inherited state that subclasses need to read or modify, and protected constructors that allow subclass instantiation while preventing arbitrary direct creation. In all these cases, protected provides targeted access — wider than private (which shuts out subclasses), narrower than public (which exposes everything to everyone).
The discipline for protected: only make a member protected when you have an identified subclass that needs it. Every protected member is a commitment to every current and future subclass author. Keep the set of protected members minimal, document them clearly, and prefer protected methods over protected fields wherever possible.
What to Read Next
Learn what happens when you don't specify an access modifier.