Access Modifiers

Su Suriya Ravichandran Updated 14 Nov 2026
8 min read ·Lesson 54 of 133

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

ModifierSame classSame packageSubclass in another packageAny 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:

MemberUsual choiceReason
FieldsprivatePreserve encapsulation; expose through methods
Constants (static final)public or privatePublic only if they are part of the API
Helper methodsprivateImplementation details
Methods for subclasses to override or callprotectedExtension points
Classes used only inside a packagepackage-privateHide implementation classes
The class's real operationspublicIts 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.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Access Modifiers and get answers drawn from it.

Signed-in readers only.