Access specifiers in Java
-
Last Updated: March 10, 2025
-
By: javahandson
-
Series
Access specifiers in Java decide who may see and use your classes, fields, methods, and constructors. Java gives you four levels: private, default, protected, and public. This guide walks through each one with runnable examples, a full comparison table, the rules that trip people up in interviews, and the mistakes worth avoiding.
Think about your house for a moment. The front porch welcomes anyone. The living room suits guests. Your bedroom stays open to family only. Your diary belongs to you alone.
Java works the same way. Every class you write holds some parts meant for the world and some parts meant only for itself. Access specifiers draw those boundaries.
Why bother? Because a field that anyone can change becomes a field that anyone can break. Once you lock a field down, you control every path that touches it. That single habit prevents a huge share of real bugs.
An access specifier is a keyword that answers a single question: from where can code reach this member? You write it in front of a class, field, method, or constructor, and the compiler enforces your answer everywhere.
Picture an office building. The lobby stays open to the public. Reaching the engineering floor needs a badge. Only two people hold keys to the server room. Your locker holds your own things.
Java hands you the same four doors. Public means the lobby. Protected means the engineering floor plus anyone who inherits a badge. Default means the floor you already work on. Private means your locker.
You will hear both terms. Most tutorials and interviewers say “access specifier”. The Java Language Specification says “access modifier”.
They point at the same four keywords, so either name works in conversation. Just remember the wider family. Java also has non-access modifiers such as static, final, abstract, and synchronized, and those change behaviour rather than visibility.
Notice the ordering. Each level opens a little wider than the one above it. Java arranges them as private, then default, then protected, then public.
You may put an access specifier on a class, a field, a method, a constructor, or a nested class. One place refuses them completely: local variables inside a method.
public class Demo {
private int field = 1; // fine
public void run() {
private int local = 2; // compile error
}
}
// error: illegal start of expressionThat rule makes sense once you think it through. A local variable dies when the method returns, and no outside code could ever reach it anyway.
Private locks a member inside the class that declares it. No other class reaches it, not even a subclass, and not even a neighbour in the same package.
Here is one small class carrying one field of each kind. We will keep returning to it.
package com.javahandson.pkg1;
public class Student {
private int rollNumber; // this class only
String name; // same package only
protected int marks; // package + subclasses
public String mainSubject; // everywhere
public Student(int rollNumber, String name, int marks) {
this.rollNumber = rollNumber;
this.name = name;
this.marks = marks;
}
public int getRollNumber() {
return rollNumber; // fine, we are inside Student
}
}Now try to read rollNumber from another class in the very same package.
package com.javahandson.pkg1;
public class Main {
public static void main(String[] args) {
Student s = new Student(101, "Suraj", 70);
System.out.println(s.getRollNumber()); // Output: 101
System.out.println(s.rollNumber); // compile error
}
}
// error: rollNumber has private access in com.javahandson.pkg1.StudentThe getter sails through. The direct field read fails. Same package, same JVM, still blocked, because private stops at the class boundary.
Here comes a detail that surprises almost everyone. One object can read the private fields of another object, as long as both share the same class.
public class Box {
private int size;
public Box(int size) {
this.size = size;
}
public boolean sameSizeAs(Box other) {
return this.size == other.size; // reading another object's private field
}
}
// usage
Box small = new Box(10);
Box large = new Box(20);
System.out.println(small.sameSizeAs(large)); // Output: falseWhy does Java permit that? Because the compiler checks the class you wrote the code in, never the object you happen to hold. Methods such as equals rely on exactly this behaviour.
Start every field as private. Widen it only when a concrete need appears. Loosening a field later costs nothing, while tightening one after other teams depend on it costs a great deal.
Leave the keyword off entirely and Java applies default access, also called package-private. The member then belongs to its package.
Java offers no word named “default” for this purpose. You simply write nothing.
String name; // package-private field
void helper() { } // package-private method
class Helper { } // package-private classCareful here. Java does own a keyword spelled default, and it shows up in switch statements and in interface methods. That keyword has nothing to do with access levels.
package com.javahandson.pkg1;
public class Main {
public static void main(String[] args) {
Student s = new Student(101, "Suraj", 70);
System.out.println("Name: " + s.name); // Output: Name: Suraj
}
}Main and Student share the package com.javahandson.pkg1, so the field opens right up.
package com.javahandson.pkg2;
import com.javahandson.pkg1.Student;
public class Main {
public static void main(String[] args) {
Student s = new Student(101, "Suraj", 70);
System.out.println(s.name); // compile error
}
}
// error: name is not public in com.javahandson.pkg1.Student;
// cannot be accessed from outside packageOne package boundary changed everything. The import statement finds the class, yet the field stays shut.
Package-private acts as a quiet middle ground. Many libraries keep whole classes at this level so users never see the moving parts.
Protected covers two groups at once. Every class in the same package qualifies. Every subclass also qualifies, even a subclass living in a far-off package.
package com.javahandson.pkg2;
import com.javahandson.pkg1.Student;
public class EngineeringStudent extends Student {
public EngineeringStudent(int rollNumber, String name, int marks) {
super(rollNumber, name, marks);
}
public void showMarks() {
System.out.println("Marks: " + marks); // inherited protected field
}
}
// usage
EngineeringStudent e = new EngineeringStudent(101, "Suraj", 70);
e.showMarks(); // Output: Marks: 70The marks field travelled across a package boundary through inheritance. Default access would have blocked it here.
Now for the sharpest edge in this whole topic. A subclass in a different package may touch a protected member only through its own type. Hold a plain parent reference and the compiler refuses.
package com.javahandson.pkg2;
import com.javahandson.pkg1.Student;
public class EngineeringStudent extends Student {
public EngineeringStudent(int rollNumber, String name, int marks) {
super(rollNumber, name, marks);
}
void compare(Student other, EngineeringStudent peer) {
System.out.println(this.marks); // fine
System.out.println(peer.marks); // fine, same subclass type
System.out.println(other.marks); // compile error
}
}
// error: marks has protected access in com.javahandson.pkg1.StudentWhat drives that restriction? Java grants the subclass access to its own inheritance, never a licence to poke at every sibling branch of the family tree. Inside the original package, no such limit applies.
Plenty of developers read protected as “subclasses only”. It actually grants strictly more than default access, never less.
An unrelated class sitting in the same package reaches a protected member with no inheritance at all. Keep that in mind when you weigh protected against default.
A protected member forms a contract with every future subclass. Change it and you break code you have never seen.
Reach for protected when a subclass genuinely must extend behaviour, such as a hook method in an abstract base class. Prefer private fields with protected methods, so children get the behaviour without holding the raw state.
Public throws the doors open. Any class in any package may use the member, provided it can see the class itself.
package com.javahandson.pkg2;
import com.javahandson.pkg1.Student;
public class Main {
public static void main(String[] args) {
Student s = new Student(101, "Suraj", 70);
s.mainSubject = "Maths"; // public field, no barrier
System.out.println("Subject: " + s.mainSubject); // Output: Subject: Maths
}
}Marking something public feels harmless in the moment. The bill arrives later.
Keep public for the handful of methods that describe what your class does. Everything supporting those methods can stay hidden.
Every rule above collapses into one grid. Learn this and you have learned the topic.
| Access Specifier | Same Class | Same Package | Subclass (Other Package) | Anywhere Else |
|---|---|---|---|---|
| private | Yes | No | No | No |
| default | Yes | Yes | No | No |
| protected | Yes | Yes | Yes | No |
| public | Yes | Yes | Yes | Yes |
Read each row from left to right and watch the doors close. Private says yes once. Public says yes four times.
One column carries a footnote. That “Subclass (Other Package)” cell for protected assumes access through the subclass type, exactly as section 5.2 showed.
Count the audience as it grows: me, my package, my children, everybody. Those four phrases map straight onto private, default, protected, and public.
Stuck on which keyword to write? Walk down this short list and stop at the first line that fits.
Notice the direction of travel. You begin closed and open up on evidence, rather than starting open and hoping to tighten later.
The four keywords stay the same everywhere. Which ones you may legally write, however, depends on what you are declaring.
A class sitting directly in a file accepts just two options.
| Specifier | Allowed? | Meaning |
|---|---|---|
| public | Yes | Visible everywhere; the file must carry the class name |
| default | Yes | Visible inside the package only |
| private | No | Nothing could ever use it |
| protected | No | A top-level class has no enclosing class to inherit from |
Nest a class inside another class and all four levels become legal. The nested class now behaves like any other member.
public class Outer {
private class PrivateInner { } // Outer only
class DefaultInner { } // same package
protected class ProtectedInner { } // package + subclasses
public class PublicInner { } // everywhere
}Fields and methods take all four specifiers with no restrictions. They follow the grid in section 7 exactly.
A constructor takes any of the four, and the choice controls who may create objects.
public class Example {
private Example() { } // nobody outside can call new
Example(int x) { } // same package
protected Example(String s) { } // package + subclasses
public Example(double d) { } // anywhere
}A private constructor powers two familiar patterns. Utility classes use it so nobody instantiates a bag of static methods. Singletons use it to funnel every caller through one factory method.
public class Singleton {
private static Singleton instance;
private Singleton() { } // the door is locked
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}One honest warning about that snippet. It shows the access idea clearly, yet two threads calling getInstance together could build two objects. Production code adds synchronization or uses an enum.
Interfaces play by their own rules, and those rules have shifted across Java versions.
public interface Payable {
int RATE = 100; // public static final
void pay(); // public abstract
default void payTwice() { // Java 8, public
log("paying twice");
pay();
pay();
}
private void log(String message) { // Java 9, hidden from implementers
System.out.println(message);
}
}So the old line “interface methods are always public” no longer holds. Since Java 9 an interface can keep helper logic to itself.
Java 9 brought the module system, and it changed what public truly means. Marking a class public no longer guarantees that other code can touch it.
A module lists the packages it shares in a file called module-info.java. Leave a package out of that list and its public classes stay invisible outside the module.
module com.javahandson.banking {
exports com.javahandson.bank; // public types here are visible outside
// com.javahandson.bank.internal is not exported,
// so its public classes stay inside this module
}Think of it as two gates in a row. Your access specifier opens the first gate, and the module declaration opens the second. Both must open before outside code gets through.
Most beginner projects run on the classpath and never notice this layer. Once you build libraries or work on a modular codebase, it matters a great deal.
Overriding adds one more rule, and interviewers love it.
class Parent {
protected void display() {
System.out.println("Parent");
}
}
class Child extends Parent {
@Override
public void display() { // protected widened to public: allowed
System.out.println("Child");
}
}class Broken extends Parent {
@Override
private void display() { } // compile error
}
// error: display() in Broken cannot override display() in Parent
// attempting to assign weaker access privileges; was protectedThe reason sits in polymorphism. Someone holding a Parent reference expects display to work. If a child could hide that method, the promise would collapse.
A private method stays invisible to subclasses, so a child never overrides one. Write a method with a matching name in the child and you create a brand new method that merely looks similar.
Add @Override to such a method and the compiler will tell you plainly that nothing gets overridden. That annotation earns its keep here.
This one bites hardest because the code reads perfectly. Your subclass sits in another package, holds a Parent variable, and touches a protected field. The compiler says no, and section 5.2 explains why.
Two objects of one class see each other completely. Beginners often expect a wall there and find none.
Private guards your design, not your secrets. Reflection can flip a field open at runtime through setAccessible, and anyone reading the class file sees the value anyway.
Never store a password or key in a private field and call the job done. Real protection needs encryption and proper secret management.
Writing default in front of a field produces a syntax error. Package-private access means writing nothing, while the default keyword belongs to switch blocks and interface methods.
A private field feels safe until a getter hands out the original object.
public class Team {
private List<String> players = new ArrayList<>();
public List<String> getPlayers() {
return players; // caller now edits your list
}
public List<String> getPlayersSafely() {
return new ArrayList<>(players); // caller edits a copy
}
}The keyword did its job. The method threw the protection away. Return a copy, or wrap the list with Collections.unmodifiableList.
Temporary public fields have a habit of turning permanent. Six months later, four modules depend on them, and nobody dares to touch the class.
A private method resists your unit test, so the quickest fix looks obvious. Bump it to public and the test compiles.
Resist that urge. A private method usually needs testing through the public method that calls it, since that path is what users actually run. When you truly must reach in, package-private plus a test class in the same package beats going public.
Let us pull all four levels into one small bank account class. Watch how each keyword earns its place.
package com.javahandson.bank;
public class BankAccount {
private final String holder; // nobody edits the owner
private double balance; // nobody edits the money
public BankAccount(String holder, double opening) {
this.holder = holder;
this.balance = opening;
}
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit must be positive");
}
balance += amount;
}
public boolean withdraw(double amount) {
if (amount <= 0 || amount > balance) {
return false;
}
balance -= amount;
return true;
}
public double getBalance() {
return balance;
}
public String status() {
return isOverdrawn() ? "Overdrawn" : "Healthy";
}
protected void applyInterest(double rate) { // subclasses tune this
balance += balance * rate;
}
private boolean isOverdrawn() { // internal detail
return balance < 0;
}
}package com.javahandson.bank;
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount("Suraj", 1000);
account.deposit(500);
System.out.println(account.getBalance()); // Output: 1500.0
boolean ok = account.withdraw(2000);
System.out.println(ok); // Output: false
System.out.println(account.getBalance()); // Output: 1500.0
System.out.println(account.status()); // Output: Healthy
// account.balance = 999999; // compile error: private
}
}The oversized withdrawal returned false and left the balance untouched. No caller could reach around the guard clause, because the field stays private.
Delete that last comment marker and the build fails immediately. Your access specifiers turned a runtime accident into a compile-time error, which is exactly the trade you want.
A: Java gives you private, default, protected, and public. Private stays inside the declaring class. Default reaches the whole package. Protected adds subclasses in other packages. Public opens to every class everywhere.
A: Default access, also called package-private, applies when you write no access keyword at all. The member then reaches every class in the same package and nothing beyond it. Java has no keyword named default for this purpose.
A: No. A top-level class takes public or default only. Private would leave the class unusable, and protected needs an enclosing class to inherit from. Nested classes, on the other hand, accept all four levels.
A: Both reach every class in the same package. Protected goes further and reaches subclasses in other packages through inheritance. So protected always grants more than default, never less.
A: No, and this catches many people. The subclass may use a protected member only through its own type or a subtype of it. Holding a plain parent-typed variable and reading the field gives a compile error. Inside the parent’s own package the restriction disappears.
A: Yes, when both objects belong to the same class. Private works per class, not per object. That rule lets methods like equals and compareTo compare internal state directly.
A: No. An overriding method must keep the parent’s visibility or widen it. Turning a protected method into public works fine, while turning it into private fails to compile with a weaker-access-privileges error.
A: Not since Java 9. Abstract, default, and static interface methods remain public, and fields remain public static final. Java 9 added private and private static interface methods so an interface can hide shared helper logic.
A: A private constructor stops outside code from calling new. Utility classes use it so nobody creates a pointless instance. Singletons use it to route every caller through one static factory method.
A: No. Private enforces a design boundary at compile time, and reflection can open the field at runtime through setAccessible. Treat it as a tool for clean structure rather than a security control.
Let us wrap up what we covered. Access specifiers in Java control who may reach your classes, fields, methods, and constructors.
Four levels run from tightest to widest: private, default, protected, and public. Private stops at the class. Default stops at the package. Protected adds subclasses anywhere, and public adds everyone.
Two subtleties separate a confident answer from a shaky one. A subclass in another package reaches protected members only through its own type. Private applies per class, so sibling objects read each other freely.
Start every field private and open it only when something real demands it. That habit alone will make your classes easier to change and far harder to break.