Skip to content
Java encapsulation 7 min read

Access Modifiers

Access modifiers are the keywords you place before a class, field, method, or constructor to control who can see and use it. They are the primary tool Java gives you to enforce encapsulation — drawing clear boundary lines between the parts of your code that are internal implementation details and the parts that form a public API.

The Four Access Levels

Java has exactly four access levels. Three have an explicit keyword; the fourth is the default when you write no keyword at all.

ModifierKeywordVisible to
PrivateprivateOnly the declaring class
Package-private(none)Any class in the same package
ProtectedprotectedSame package + subclasses (any package)
PublicpublicEveryone

Think of them as progressively wider circles of visibility.

Tip: The golden rule: start with the most restrictive access level you can get away with (private), and only widen it when you have a concrete reason.


private — Class-Level Encapsulation

private members are visible only inside the class that declares them. Not to subclasses, not to classes in the same package — only to the class itself.

public class Counter {
    private int count = 0;   // hidden from all outside code

    public void increment() {
        count++;              // OK — same class
    }

    public int getCount() {
        return count;         // controlled read access
    }
}

class App {
    public static void main(String[] args) {
        Counter c = new Counter();
        c.increment();
        System.out.println(c.getCount()); // OK
        // c.count = 99; // Compile error: count has private access in Counter
    }
}

Output:

1

Use private for all fields by default, and for helper methods that are pure implementation details. See the Encapsulation page for the full pattern of pairing private fields with public getters and setters.


Package-Private (Default) — Package-Level Collaboration

When you write no modifier at all, the member is package-private — visible to all classes in the same package, but invisible outside it.

package com.example.billing;

class Invoice {              // package-private class
    double amount;           // package-private field

    void print() {           // package-private method
        System.out.println("Invoice: $" + amount);
    }
}
package com.example.billing;

class InvoiceProcessor {    // same package — can access Invoice freely
    void process() {
        Invoice inv = new Invoice();
        inv.amount = 250.00;
        inv.print();
    }
}
package com.example.shipping;

// import com.example.billing.Invoice; // Compile error: Invoice is not public

Package-private is useful for classes and helpers that are internal to a module but need to cooperate with several classes in the same package. Packages explains how to organize your code into packages to take full advantage of this.

Note: Top-level classes (not nested) can only be public or package-private — you cannot declare a top-level class private or protected.


protected — Inheritance Access

protected members are visible to:

  1. All classes in the same package (just like package-private).
  2. Any subclass, even if it lives in a different package.
package com.example.shapes;

public class Shape {
    protected String color;   // subclasses can read/write this

    protected void describe() {
        System.out.println("Shape color: " + color);
    }
}
package com.example.drawing;    // different package!

import com.example.shapes.Shape;

public class Circle extends Shape {
    private double radius;

    public Circle(String color, double radius) {
        this.color = color;        // OK: inherited protected field
        this.radius = radius;
    }

    public void draw() {
        describe();                // OK: inherited protected method
        System.out.println("Circle radius: " + radius);
    }
}
public class Main {
    public static void main(String[] args) {
        Circle c = new Circle("red", 5.0);
        c.draw();
        // c.color = "blue";  // Compile error: not accessible from unrelated class
    }
}

Output:

Shape color: red
Circle radius: 5.0

Warning: A common misconception — protected is not accessible from an arbitrary class in another package just because it’s a “weaker” public. An unrelated class in another package still cannot touch a protected member. Only subclasses (and same-package classes) get access.

protected is the right choice when you are designing a class for inheritance and want subclasses to be able to customize or read internal state, without opening it up to everyone.


public — Open API

public members are accessible from any class anywhere, as long as the class itself is also reachable (i.e., the class must also be public for cross-package access to work).

package com.example.math;

public class MathUtils {
    public static int add(int a, int b) {
        return a + b;
    }

    public static double sqrt(double x) {
        return Math.sqrt(x);
    }
}
import com.example.math.MathUtils;

public class App {
    public static void main(String[] args) {
        System.out.println(MathUtils.add(3, 4));   // 7
        System.out.println(MathUtils.sqrt(16));    // 4.0
    }
}

Output:

7
4.0

Mark methods and classes public when they are intentionally part of the API you want others to use. Avoid making fields public — this bypasses encapsulation entirely, leaving you no control over how they are read or modified.


Quick Visual Reference

Here is a complete at-a-glance table. “Y” means accessible, “N” means not accessible.

Locationprivatepackage-privateprotectedpublic
Same classYYYY
Same package (different class)NYYY
Subclass (different package)NNYY
Unrelated class (different package)NNNY

Applying Modifiers to Classes, Constructors, and Methods

Access modifiers apply consistently to all member types, but there are a few nuances worth knowing:

Top-Level Classes

Only public or package-private (no modifier). Making a class public means it must live in a file with the same name.

Constructors

A private constructor prevents instantiation from outside the class — the basis of the Singleton pattern and factory methods.

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}   // nobody can call new Singleton() from outside

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

Nested / Inner Classes

Inner classes can use all four access modifiers, including private (common for internal helper classes that should be completely hidden from outside code) and protected (for helper classes meant for subclasses).


Under the Hood

Access control in Java operates at two levels: compile time and runtime.

At compile time, javac checks every member access against the declared access flags and rejects illegal access with a compile error — this is where you normally encounter these rules.

In the bytecode, each class, field, and method carries an access_flags bitmask in the .class file. Flags like ACC_PUBLIC (0x0001), ACC_PRIVATE (0x0002), and ACC_PROTECTED (0x0004) are recorded there. You can inspect these with the javap tool:

javap -p -verbose Counter.class

At runtime, the JVM verifier re-checks access flags when loading and linking classes. Violations throw IllegalAccessError — a subclass of Error, not Exception. This protects against classes that were compiled correctly but whose bytecode was later tampered with.

The Reflection API can bypass access modifiers by calling setAccessible(true) on a Field or Method. This works for legitimate use cases (testing, frameworks), but Java 9’s module system (JPMS) added an additional layer of enforcement — strong encapsulation that even reflection cannot always breach without explicit module exports. See Java 9: Modules for details.

Note: Performance-wise, private methods have a small advantage in HotSpot: the JIT compiler knows they cannot be overridden and will eagerly inline them. Non-private, non-final virtual methods require vtable dispatch — see vtable & Dynamic Dispatch for a deeper look.


Common Mistakes

  • Making fields public for convenience. This is the most common encapsulation mistake. Always use private fields with getters/setters instead.
  • Confusing protected with “accessible to anyone in other packages.” Remember: only subclasses (and same-package classes) can touch protected members.
  • Forgetting that package-private is a real boundary. Code in a different package without subclassing gets zero access to package-private members — very useful for internal APIs.
  • Using public on everything. This is the “no encapsulation” approach and leads to tightly coupled, hard-to-change code.

  • Encapsulation — The OOP principle that access modifiers enforce; the bigger picture.
  • Packages — How to organize classes into packages, which determines what “same package” means for access rules.
  • Inheritance — Where protected access becomes essential for designing extensible class hierarchies.
  • Inner Classes — Nested classes can be private, making them completely invisible outside the enclosing class.
  • Reflection API — The advanced mechanism that can bypass access modifiers at runtime.
  • Java 9: Modules — JPMS adds a stronger layer of encapsulation on top of access modifiers for large-scale applications.
Last updated June 13, 2026
Was this helpful?