Access Modifiers in Java
Access Modifiers in Java
You met access modifiers briefly in the Beginner tutorial, mostly as a way to protect a class's fields. At the Intermediate level, the focus shifts to how these modifiers interact with inheritance, and the design judgement calls that experienced developers make when deciding how open or closed a piece of code should be.
public, private, protected, and default
Java gives you exactly four visibility levels, and choosing between them is really a choice about how much of your class's internals you are willing to expose to the rest of the codebase.
Modifier | Visible From |
public | Anywhere in any class, in any package |
protected | Same package, plus subclasses in other packages |
default (no keyword) | Same package only |
private | Same class only |
package com.app.account;
public class Account {
public String accountHolder;
protected double balance;
String branchCode; // default access
private String pin;
private boolean validatePin(String entered) {
return entered.equals(pin);
}
}
A useful mental model: public is your class's storefront, visible to every customer. private is the back office, visible only to staff working that exact desk. protected and default sit in between, sized for 'family' (subclasses) and 'team' (same package) access respectively.
Access Control in Inheritance
Access modifiers do not just control visibility; they also constrain what a subclass is allowed to do when it overrides an inherited method. Java enforces one firm rule here: an overriding method can keep the same access level as the superclass version, or make it more open, but it can never make it more restrictive.
class Vehicle {
protected void start() {
System.out.println("Vehicle starting");
}
}
class Car extends Vehicle {
@Override
public void start() { // widening protected -> public is legal
System.out.println("Car starting");
}
}
class Bike extends Vehicle {
@Override
// private void start() { } // INVALID — narrowing protected -> private won't compile
void start() {
System.out.println("Bike starting");
}
}
There is a second, separate effect worth knowing: private members are not inherited at all in the usual sense. A subclass cannot see or override a superclass's private fields or methods, since they are invisible to it entirely, even though the subclass object technically still contains that private data in memory.
Best Practices for Access Modifiers
• Start as restrictive as possible. Default to private, and only widen access (to default, then protected, then public) when a real need arises.
• Treat public members as a promise. Once a public field or method ships, other code may depend on it; changing or removing it later can break callers you don't control.
• Use protected sparingly and intentionally. It exposes internals to any future subclass, even ones written by other developers, so reserve it for members genuinely meant to be extended.
• Prefer default (package-private) for internal helper classes. If a class exists purely to support others in the same package, there is rarely a reason to make it public.
• Expose behaviour, not state. Favour public methods that act as public fields that let outside code manipulate internal data directly; this keeps the encapsulation benefits from Chapter 4 of the Beginner tutorial intact.
Did You Know?
This same narrowing restriction, that an override cannot reduce visibility, also applies to checked exceptions in reverse: an overriding method can throw fewer or narrower checked exceptions than the superclass version, but never broader ones. Both rules exist for the same reason, to guarantee that any code already trusting the superclass's contract continues to work safely with any subclass.










