Access Modifiers
Access modifiers control which code is allowed to use a class, field, method, or constructor. They are how Java enforces encapsulation: a class decides what is part of its public contract and what is an internal detail that other code must not depend on. Java has four access levels, three written as keywords and one expressed by writing no modifier at all.
The Four Access Levels
| Modifier | Same class | Same package | Subclass in another package | Any other code |
|---|---|---|---|---|
private | ✓ | |||
| (none), package-private | ✓ | ✓ | ||
protected | ✓ | ✓ | ✓ | |
public | ✓ | ✓ | ✓ | ✓ |
From most restrictive to least: private → package-private → protected → public.
private
A private member is accessible only inside the class that declares it (including its nested classes).
public class Wallet {
private double balance;
public void add(double amount) {
if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
balance += amount;
}
public double getBalance() { return balance; }
private void audit(String msg) { // internal helper
System.out.println("[audit] " + msg);
}
}
Wallet w = new Wallet();
w.add(100);
w.balance = 1_000_000; // compile error: balance has private access in Wallet
w.audit("x"); // compile error: audit has private access in Wallet
Use private for:
- All fields, as the default. Expose data through methods that can validate and control it.
- Helper methods that exist only to support the class's own public methods.
- Constructors that should not be called directly, forcing creation through static factory methods, or preventing instantiation of utility classes.
Access is checked per class, not per object. A method of Wallet can read the private balance of another Wallet:
public boolean hasMoreThan(Wallet other) {
return this.balance > other.balance; // allowed: same class
}
Package-Private (No Modifier)
A member with no access modifier is visible to every class in the same package, and to nothing outside it.
package com.gocourse.billing;
class TaxCalculator { // package-private class
static double gst(double amount) { // package-private method
return amount * 0.18;
}
}
package com.gocourse.billing;
public class InvoiceService {
public double total(double subtotal) {
return subtotal + TaxCalculator.gst(subtotal); // allowed: same package
}
}
package com.gocourse.web;
import com.gocourse.billing.TaxCalculator; // compile error: TaxCalculator is not public
Package-private access lets classes within one package collaborate closely while presenting only their public classes and methods to the rest of the application. InvoiceService is the package's public face; TaxCalculator is an implementation detail that can be changed or removed freely.
Packages are not hierarchical for access purposes: com.gocourse.billing.internal cannot see package-private members of com.gocourse.billing, despite the similar name.
Package-private is also the default for unit tests: test classes are usually placed in the same package (in a separate source folder), so they can test package-private code without making it public.
protected
A protected member is visible within its package and to subclasses in any package. It is meant for members that subclasses need but general callers should not use.
package com.gocourse.reports;
public abstract class Report {
private final String title;
protected Report(String title) { // only subclasses construct reports
this.title = title;
}
protected abstract String body(); // subclasses implement this
protected String formatHeading(String text) { // helper offered to subclasses
return "## " + text;
}
public final String render() {
return formatHeading(title) + "\n" + body();
}
}
package com.gocourse.sales;
import com.gocourse.reports.Report;
public class MonthlySales extends Report {
public MonthlySales() {
super("Monthly Sales"); // allowed: protected constructor via super
}
@Override
protected String body() {
return formatHeading("Totals") + "\n..."; // allowed: inherited protected method
}
}
Code that merely uses reports, without subclassing, can call only render():
Report r = new MonthlySales();
r.render(); // allowed: public
r.formatHeading("x"); // compile error from another package that is not a subclass
A subtle rule about protected
From a subclass in a different package, a protected instance member can be accessed only through references whose type is that subclass (or its own subclasses), not through an arbitrary reference to the parent type:
package com.gocourse.sales;
public class MonthlySales extends Report {
void demo(Report other, MonthlySales same) {
formatHeading("ok"); // allowed: on this object
same.formatHeading("ok"); // allowed: reference of the subclass type
other.formatHeading("no"); // compile error: other might be some unrelated subclass
}
}
The rule stops a subclass from reaching into the protected internals of sibling classes it knows nothing about.
protected is a commitment
A protected member is part of the API that subclasses depend on, so it is nearly as hard to change as a public one. Prefer private fields with protected methods, rather than protected fields, so the parent class keeps control over its data.
public
A public class or member is accessible from anywhere.
public class CurrencyFormatter {
public static String inr(double amount) {
return String.format("₹%,.2f", amount);
}
}
Public members form the class's contract with the rest of the program, and for libraries, with every project that uses the library. Changing or removing them breaks callers, so keep the public surface as small as the class's purpose allows.
Access Modifiers on Top-Level Classes
A top-level class (one not nested in another) can be only public or package-private:
public class Order { } // usable everywhere; file must be Order.java
class OrderValidator { } // usable only within the package
private class Hidden { } // compile error: private not allowed on a top-level class
Nested classes can use all four levels, because they are members of their enclosing class.
Overriding and Access
An overriding method cannot be more restrictive than the method it overrides; it may be equally or less restrictive:
public class Animal {
protected String sound() { return "..."; }
}
public class Dog extends Animal {
@Override
public String sound() { return "Woof"; } // allowed: protected → public (wider)
}
public class Cat extends Animal {
@Override
private String sound() { return "Meow"; } // compile error: attempting to assign weaker access
}
The rule protects polymorphism. Code holding an Animal reference may call sound() wherever Animal allows it. If a subclass could narrow access, that call would become illegal for some objects, which the compiler could not detect from the declared type.
Methods declared in an interface are public, so every implementing method must be public.
Choosing the Right Level
Start with the most restrictive level that works, and widen only when a real need appears:
| Member | Usual choice | Reason |
|---|---|---|
| Fields | private | Preserve encapsulation; expose through methods |
Constants (static final) | public or private | Public only if they are part of the API |
| Helper methods | private | Implementation details |
| Methods for subclasses to override or call | protected | Extension points |
| Classes used only inside a package | package-private | Hide implementation classes |
| The class's real operations | public | Its contract |
Narrowing access later is a breaking change; widening it never is. Starting restrictive keeps options open.
Access Modifiers and Modules
Since Java 9, modules add a layer above packages. A public class in a package that its module does not export is not accessible to other modules, even though it is public. This lets a library keep internal packages truly internal. Classpath-based applications, which include most beginner and many production programs, are unaffected. Modules are covered in the Modules and Packages section.
Practical Example: A Small Library Package
// file: com/gocourse/library/Book.java
package com.gocourse.library;
public class Book {
private final String isbn;
private final String title;
private boolean onLoan; // private: changed only via Library
public Book(String isbn, String title) {
this.isbn = isbn;
this.title = title;
}
public String getIsbn() { return isbn; }
public String getTitle() { return title; }
public boolean isOnLoan() { return onLoan; }
void setOnLoan(boolean onLoan) { // package-private: only Library uses it
this.onLoan = onLoan;
}
}
// file: com/gocourse/library/Library.java
package com.gocourse.library;
import java.util.HashMap;
import java.util.Map;
public class Library {
private final Map<String, Book> catalog = new HashMap<>();
private final Map<String, String> loans = new HashMap<>(); // isbn -> member
public void add(Book book) {
catalog.put(book.getIsbn(), book);
}
public boolean lend(String isbn, String member) {
Book book = catalog.get(isbn);
if (book == null || book.isOnLoan()) {
return false;
}
book.setOnLoan(true); // allowed: same package
loans.put(isbn, member);
log("LEND " + isbn + " to " + member);
return true;
}
public boolean giveBack(String isbn) {
Book book = catalog.get(isbn);
if (book == null || !book.isOnLoan()) {
return false;
}
book.setOnLoan(false);
loans.remove(isbn);
log("RETURN " + isbn);
return true;
}
private void log(String event) {
System.out.println("[library] " + event);
}
}
// file: com/gocourse/app/Main.java
package com.gocourse.app;
import com.gocourse.library.Book;
import com.gocourse.library.Library;
public class Main {
public static void main(String[] args) {
Library lib = new Library();
Book book = new Book("978-0134685991", "Effective Java");
lib.add(book);
System.out.println(lib.lend("978-0134685991", "Ada")); // true
System.out.println(lib.lend("978-0134685991", "Linus")); // false: already lent
System.out.println(book.isOnLoan()); // true
// book.setOnLoan(false); // compile error: not public outside the package
// lib.log("hack"); // compile error: private
}
}
Output:
[library] LEND 978-0134685991 to Ada
true
false
true
Each access level has a job. Book's fields are private. Its getters are public, so any code can read a book's state. setOnLoan is package-private: Library, in the same package, can change loan status, but Main cannot mark a book returned without going through Library.giveBack, which also updates the loan records. log is private because it is an internal detail of Library. The access modifiers guarantee that the catalog and the loan records can never disagree, regardless of what code in other packages does.