JAVA / CLASSES, CONSTRUCTORS AND ENCAPSULATION
Access modifiers and choosing the right visibility
Choose deliberately between private, package-private, protected and public, and predict exactly which code can touch a member before the compiler tells you.
What you will learn
- Name the exact region of code each of the four Java access levels opens up
- Read a private field on another instance from inside the same class
- Pick protected knowing it also exposes the member to every class in the package
- Start each member private and widen one level at a time until the caller compiles
Understanding Access modifiers and choosing the right visibility
Java has four visibility levels, and each names a region of source code rather than a runtime condition. private restricts a member to the body of the top-level class it is declared in; no modifier at all, called package-private, opens it to every class whose package declaration matches exactly; protected adds subclasses in any package on top of that package access; public opens it to any code that can already see the class itself. The compiler answers every access question by asking where the accessing code sits, which is why the same expression can be legal in one file and rejected in another.
Read a modifier as a promise about change. A private field can be renamed, retyped or deleted after reading one class; a public field cannot be touched without breaking callers you have never seen. That asymmetry is why the productive default is to declare a member private and widen it only when a real caller fails to compile, one level at a time. Visibility also stacks: a public method on a package-private class is still unreachable from another package, because a caller must be able to see the class before the member's modifier matters at all.
Two rules regularly surprise people. private is enforced per class, not per object, so inside Ledger the expression other.cents compiles even though other is a different instance, and the same relaxation lets nested classes and their enclosing class read each other's private members. protected is not a narrower private but a wider package-private: it grants access to subclasses anywhere plus every class in the same package, so a protected field is writable by the classes sitting beside it. An override may widen visibility but never narrow it, because code holding a supertype reference must keep working.
Choosing well is mostly about resisting the reflex to widen. When a compile error appears, the question is not "how do I make this legal" but "who is the caller, and does it deserve to see this".
class Ledger {
private int cents; // only code inside Ledger
int entryCount; // no modifier: any class in this package
protected String currency; // this package, plus subclasses anywhere
public String label; // any code that can see Ledger
Ledger(String label, int cents) {
this.label = label;
this.cents = cents;
this.currency = "USD";
this.entryCount = 1;
}
boolean richerThan(Ledger other) {
return this.cents > other.cents; // another object's private field
}
public String describe() {
return label + ": " + cents + " cents " + currency;
}
}
public class Main {
public static void main(String[] args) {
Ledger mine = new Ledger("mine", 1200);
Ledger yours = new Ledger("yours", 900);
System.out.println(mine.describe());
System.out.println(mine.richerThan(yours));
mine.entryCount++; // same package, so this is allowed
System.out.println(mine.entryCount);
// System.out.println(mine.cents); // error: cents has private access in Ledger
}
}An access modifier names the region of source code allowed to mention a member, so choosing one decides how much of your design you have promised not to change.
Worked examples
Subclasses see protected, never private
Shows which inherited members a subclass may name, and that an override can widen visibility but not shrink it.
class Sensor {
private String serial = "S-1";
protected int reading = 21;
protected String tag() { return "sensor"; }
}
class Thermostat extends Sensor {
String report() {
// return serial; // error: serial has private access in Sensor
return tag() + ":" + reading;
}
@Override
public String tag() { return "thermostat"; }
}
public class Main {
public static void main(String[] args) {
System.out.println(new Thermostat().report());
}
}Example explained
Line 1reading is protected, so Thermostat can name it unqualified, and that would still hold if Thermostat lived in a different package.
Line 2serial is private to Sensor, so the commented line fails with "serial has private access in Sensor" even though Thermostat is a subclass.
Line 3tag() is overridden as public, which widens access; making it package-private or private in Thermostat fails with "attempting to assign weaker access privileges; was protected".
Line 4report() calls tag() and gets the Thermostat version, because the override replaces the implementation regardless of the modifier change.
Nested classes share one private boundary
Demonstrates that private is scoped to the whole top-level class body, so an enclosing class and its nested class can read each other's private members.
public class Main {
private static int totalHits = 0;
private static class Counter {
private int hits = 0;
void record() {
hits++;
totalHits++; // reaches Main's private static field
}
}
public static void main(String[] args) {
Counter c = new Counter();
c.record();
c.record();
System.out.println(c.hits + " " + totalHits); // Main reads Counter's private field
}
}Example explained
Line 1Counter.record() increments totalHits, a private static field of the enclosing class Main.
Line 2main reads c.hits, a private field of the nested class, so the permission runs in both directions.
Line 3Both work because private is scoped to the top-level class body, including everything nested inside it.
Line 4Since Java 11 the JVM allows this directly through nestmate access; older compilers emitted hidden synthetic accessor methods to achieve the same effect.
protected also means package-wide
Shows a non-subclass calling a protected method purely because it sits in the same package.
class Engine {
protected void start() { System.out.println("engine started"); }
}
class Toolkit {
void boot(Engine e) { e.start(); }
}
public class Main {
public static void main(String[] args) {
new Toolkit().boot(new Engine());
}
}Example explained
Line 1Toolkit does not extend Engine, so inheritance grants it nothing here.
Line 2The call still compiles because protected includes everything package-private allows, and both classes are in the same package.
Line 3Changing start() to private makes the same line fail with "start() has private access in Engine", which is the level people usually mean when they reach for protected.
Important notes
A top-level class may only be public or have no modifier; writing private or protected there fails with "modifier private not allowed here". All four levels apply to members and to nested classes.
These are compile-time checks on names, not a security boundary: reflection with setAccessible can still reach a private field when module and security settings permit it.
Common mistakes
Turning a field public to silence "cents has private access in Ledger". The field's name and type become part of the API, so renaming or retyping it later breaks every caller instead of one class.
Reading protected as "subclasses only". Every class in the same package can call it too, so a protected setter quietly hands package-wide write access to your state.
Assuming a member with no modifier is public because everything compiles today. It compiles only while the caller stays in the same package; moving one class out yields "start() is not public in Engine; cannot be accessed from outside package".
Try it yourself
Change, predict, then run
In one file write class Door with a private boolean locked, a protected void unlock() that sets it false, and a public boolean isOpen(); then add a class Inspector in the same file that calls unlock() and also tries to read locked directly. Run it, note which of the two lines the compiler rejects, and explain why the other compiles even though Inspector is not a subclass.
Open the Java workspaceCheck your understanding
Inside class Money, the method boolean above(Money other) { return this.cents > other.cents; } compiles even though cents is private. Why?
- this and other point at the same object, so only one object's private field is really read.
- private only blocks other packages, so any class in Money's package can read cents.
- private access is granted per class, so code inside Money may read cents on any Money instance.
- The compiler silently generates a getter for private fields used within the same file.
Show answer
Access is checked against the class the code sits in, not the object the field belongs to, so inside Money's body the cents of every Money instance is reachable. The package answer describes package-private, a strictly wider level: a private member stays invisible to other classes in the same package and to subclasses.