What are the differences between public, protected, package-private (default), and private access modifiers in Java, and what are the best practices for using each when designing classes, interfaces, and handling inheritance?
Java access modifiers control visibility and accessibility of classes, methods, and variables, with private being most restrictive (only within the same class), package-private (default) allowing access within the same package, protected extending to subclasses regardless of package, and public providing universal access across the entire application. Best practices recommend using private for implementation details, protected for inheritance relationships, package-private for internal package utilities, and public only for necessary APIs that need to be accessed from outside the package.
Contents
- Understanding Access Modifiers in Java
- Detailed Comparison of Access Modifiers
- Inheritance and Access Modifiers
- Best Practices for Access Modifiers
- Common Pitfalls and Solutions
- Advanced Scenarios and Patterns
Understanding Access Modifiers in Java
Java provides four access modifiers that determine the visibility of classes, methods, constructors, and fields. These modifiers are fundamental to encapsulation, one of the core principles of object-oriented programming. Each modifier serves a specific purpose in controlling how code can interact with your components.
The access modifiers in Java work at two levels:
- Class level: Determines which classes can access the class itself
- Member level: Determines which classes can access methods, fields, and constructors within a class
Key Insight: Access modifiers in Java are primarily enforced at compile-time, though some restrictions (like private members) are enforced at runtime through the Java Virtual Machine’s security mechanisms.
Java’s access control system is designed to promote loose coupling and high cohesion in code design. By restricting access to implementation details, developers can change internal code without affecting external code that depends on the public interface.
Detailed Comparison of Access Modifiers
Private Access Modifier
The private modifier is the most restrictive access level in Java. Members declared as private are only accessible within the declaring class itself.
public class BankAccount {
private double balance; // Only accessible within BankAccount class
private void validateAmount(double amount) {
// Only accessible within BankAccount class
}
}
Characteristics:
- Visibility: Only within the same class
- Inheritance: Not inherited by subclasses
- Best used for: Internal implementation details, sensitive data, helper methods
Package-Private (Default) Access Modifier
When no access modifier is specified, Java uses package-private (also called default) access. Members with package-private access are accessible to any class within the same package.
// No access modifier specified
class UtilityClass {
String internalUtility; // Accessible within the same package
void performOperation() {
// Accessible within the same package
}
}
Characteristics:
- Visibility: Within the same package
- Inheritance: Inherited by subclasses in the same package
- Best used for: Internal package utilities, package-level helper classes
Protected Access Modifier
The protected modifier allows access within the same package and to subclasses in different packages.
public class Vehicle {
protected int engineSize; // Accessible in Vehicle class, same package, and subclasses
protected void startEngine() {
// Accessible in Vehicle class, same package, and subclasses
}
}
Characteristics:
- Visibility: Same package + subclasses (any package)
- Inheritance: Inherited by subclasses
- Best used for: Methods and fields that should be accessible to subclasses but not to unrelated classes
Public Access Modifier
The public modifier provides the least restrictive access, allowing access from any class in any package.
public class UserService {
public User createUser(String username) {
// Accessible from anywhere
return new User(username);
}
}
Characteristics:
- Visibility: From anywhere
- Inheritance: Always inherited by subclasses
- Best used for: Public APIs, interfaces, service classes
Comparison Table
| Access Modifier | Class Access | Package Access | Subclass Access | World Access |
|---|---|---|---|---|
| private | ✓ | ✗ | ✗ | ✗ |
| package-private | ✗ | ✓ | ✓ (same package only) | ✗ |
| protected | ✗ | ✓ | ✓ (any package) | ✗ |
| public | ✓ | ✓ | ✓ | ✓ |
Note: Inner classes can be private, protected, or package-private, but top-level classes can only be public or package-private.
Inheritance and Access Modifiers
Inheritance relationships significantly impact how access modifiers work. Understanding these interactions is crucial for proper class design.
Access Rules in Inheritance
When a subclass inherits from a superclass, the access rules change:
- Private members: Never inherited or accessible
- Package-private members: Inherited if subclass is in same package
- Protected members: Always inherited and accessible
- Public members: Always inherited and accessible
// Package com.example.vehicle
public class Vehicle {
private String serialNumber; // Not inherited
protected int speed; // Inherited
public String model; // Inherited
protected void setSpeed(int s) {
speed = s;
}
}
// Package com.example.car
public class Car extends Vehicle {
public void accelerate() {
// speed is accessible (protected)
setSpeed(speed + 10); // Method is accessible (protected)
// serialNumber is NOT accessible (private)
}
}
Method Overriding Rules
When overriding methods, access modifiers must follow these rules:
- Cannot reduce visibility: A subclass method cannot be more restrictive than the superclass method
- Can increase visibility: A subclass method can be more public than the superclass method
public class Vehicle {
protected void start() {
// Protected method
}
}
public class Car extends Vehicle {
// Valid - increasing visibility to public
@Override
public void start() {
System.out.println("Car starting...");
}
}
public class Motorcycle extends Vehicle {
// Invalid - cannot reduce visibility from protected to private
// private void start() { } // COMPILE ERROR!
}
Constructor Access and Inheritance
Constructors are not inherited, but the accessibility of superclass constructors affects subclass initialization:
public class Vehicle {
private Vehicle() {
// Private constructor - cannot be extended
}
}
// COMPILE ERROR - cannot subclass Vehicle
public class Car extends Vehicle {
// Error: There is no default constructor available in Vehicle
}
Best Practices for Access Modifiers
Private Modifier Best Practices
When to use private:
- Internal implementation details that don’t need external exposure
- Sensitive data fields that require validation
- Helper methods used only within the class
- Constants that should not be modified
Example:
public class BankAccount {
private double balance;
private String accountNumber;
private void validateAmount(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
}
public void deposit(double amount) {
validateAmount(amount);
balance += amount;
}
}
Package-Private Best Practices
When to use package-private:
- Internal utility classes used only within a package
- Helper classes that shouldn’t be exposed outside the package
- Package-level constants and configurations
- Implementation details that are shared across package classes
Example:
// Package: com.example.internal
class InternalUtils {
static void logError(String message) {
// Package-private utility method
}
}
// Package: com.example.services
public class OrderService {
public void processOrder(Order order) {
InternalUtils.logError("Processing order: " + order.getId());
}
}
Protected Modifier Best Practices
When to use protected:
- Methods that need to be overridden by subclasses
- Fields that subclasses need to access
- Template method pattern implementations
- Extension points in frameworks and APIs
Example:
public abstract class PaymentProcessor {
protected abstract boolean validatePayment(Payment payment);
protected void logTransaction(String transactionId) {
// Common logging implementation
}
public final void process(Payment payment) {
if (validatePayment(payment)) {
// Process payment
logTransaction(payment.getTransactionId());
}
}
}
Public Modifier Best Practices
When to use public:
- Public APIs and service interfaces
- Main entry points for functionality
- Data transfer objects (DTOs)
- Factory classes and builders
Example:
public class UserService {
public User createUser(String username, String email) {
// Public API method
return new User(username, email);
}
public List<User> getAllUsers() {
// Public API method
return userRepository.findAll();
}
}
Recommended Access Levels in Order of Preference
- Private - Safest choice, start here
- Package-private - Good for internal package utilities
- Protected - Use when subclassing is required
- Public - Use only when absolutely necessary
Common Pitfalls and Solutions
Overusing Public Modifiers
Problem: Making everything public leads to tight coupling and fragile APIs.
Solution: Follow the principle of “public as a last resort.” Make fields and methods private by default, then relax access only when necessary.
// Bad - Too public
public class ShoppingCart {
public List<Item> items; // External code can modify directly
public void addItem(Item item) { /*...*/ }
}
// Better - Proper encapsulation
public class ShoppingCart {
private final List<Item> items = new ArrayList<>();
public void addItem(Item item) {
// Add validation logic
items.add(item);
}
public List<Item> getItems() {
return Collections.unmodifiableList(items);
}
}
Misusing Protected Members
Problem: Using protected when package-private would suffice can unnecessarily expose implementation details.
Solution: Only use protected when you explicitly want to enable subclassing from other packages.
// Potentially problematic
public class DataProcessor {
protected List<String> processData(List<String> input) {
// This exposes implementation to subclasses in other packages
}
}
// Better approach
public class DataProcessor {
// Use package-private if only same package subclasses need access
List<String> processData(List<String> input) {
// Implementation
}
}
Ignoring Inheritance Access Rules
Problem: Forgetting that package-private members aren’t accessible to subclasses in different packages.
Solution: Be explicit about access levels when designing for inheritance.
// Problematic design
package com.example.core;
public class BaseClass {
// Package-private - not accessible to subclasses in other packages
void internalMethod() { }
}
package com.example.extension;
public class ExtendedClass extends BaseClass {
// COMPILE ERROR - cannot access internalMethod()
public void doSomething() {
internalMethod(); // Error!
}
}
// Better design
package com.example.core;
public class BaseClass {
// Protected - accessible to subclasses in any package
protected void internalMethod() { }
}
Advanced Scenarios and Patterns
Builder Pattern with Access Modifiers
The builder pattern demonstrates effective use of access modifiers to control object construction:
public class User {
private final String username;
private final String email;
private final String firstName;
private final String lastName;
// Private constructor - only accessible via Builder
private User(Builder builder) {
this.username = builder.username;
this.email = builder.email;
this.firstName = builder.firstName;
this.lastName = builder.lastName;
}
// Static nested Builder class - package-private by default
static class Builder {
private String username;
private String email;
private String firstName;
private String lastName;
public Builder username(String username) {
this.username = username;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public User build() {
return new User(this);
}
}
}
Package-Level Interfaces
Sometimes you want to define interfaces that are only accessible within the same package:
// Package: com.example.service
interface InternalService {
void performInternalOperation();
}
// Package: com.example.service
public class ServiceImpl implements InternalService {
@Override
public void performInternalOperation() {
// Implementation
}
}
// Package: com.example.client
// Cannot access InternalService - compile error
// public class Client {
// InternalService service; // Error!
// }
Module System (Java 9+)
Java 9 introduced the module system, which adds another layer of access control:
module com.example.core {
// Exports packages to specific modules
exports com.example.api to com.example.client;
// Opens packages for reflection
opens com.example.internal for reflection;
}
Conclusion
Key Takeaways:
- Private should be your default choice for implementation details and sensitive data
- Package-private works well for internal package utilities and shared implementation
- Protected enables proper inheritance relationships while maintaining some encapsulation
- Public should be used sparingly for well-defined APIs and entry points
Practical Recommendations:
- Start with the most restrictive access modifier (private) and relax only when necessary
- Consider the package structure when deciding between protected and package-private
- Be mindful of inheritance relationships when designing class hierarchies
- Document your public APIs thoroughly, as they form contracts with external code
- Regularly review access levels during code refactoring to maintain proper encapsulation
Related Questions Answered:
- Can interfaces have access modifiers? Interface methods are implicitly public, and interface fields are implicitly public, static, and final.
- What happens if I try to access a private member from outside the class? You’ll get a compile-time error.
- How do access modifiers affect serialization? Private fields are included in serialization by default, but you can control this with
transientmodifiers or custom serialization logic.
Understanding and properly applying Java access modifiers is fundamental to writing maintainable, secure, and well-designed object-oriented code. By following these principles, you’ll create systems that are easier to understand, modify, and extend over time.