---
title: 'Access Modifiers'
source: 'https://academia.sh/en/courses/java-object-model/access-modifiers'
course: 'Object-Oriented Java'
language: en
updated: '2026-08-17T18:09:40+00:00'
license: 'CC BY-SA 4.0'
---

# Access Modifiers

Crossing four visibility levels with four call positions, ten of sixteen pairs compile; the whole decision is made at compile time and the member stays right where it is at runtime — all four fields sit in the class file, and only a flag carries the distinction. Protected level also opens up the package.

The previous lesson measured the moment an object is constructed, and at its end a modifier
came out of nowhere: because a method marked `private` was never inherited by the subclass,
the constructor trap never arose there. That single word switched a decision's side, but the
word itself was never measured.

This lesson measures it. Its question is: who decides which side a member is **seen from**,
and when is that decision made? The answer is more definite here than this course tends to
give. Visibility is the most extreme case among the eight decisions the course measures: the
whole decision belongs to the **declared type** and to compile time — runtime contributes
nothing at all. The measurement shows this from two directions — by counting pairs that
compile and pairs that do not, then by looking at where the member sits at runtime.

## Whose Question Is Visibility

Java recognizes four visibility levels, and three of them have a name: `private`,
`protected`, `public`. The fourth has no name — it is the level in effect when no modifier is
written at all, and it is called **package-private**. This namelessness is not a detail:
leaving a member unmarked is not failing to make a choice, it is choosing to leave it open to
the package.

Levels have no meaning on their own; meaning is born only together with **where access is
attempted from**. This is why the measurement is a crossing: each of the four levels is tried
from four separate positions. The positions are — inside the class declaring the member, an
unrelated class in the same package, a subclass in another package, an unrelated class in
another package. Sixteen pairs come out, and each pair reduces to a single question: does
this source compile?

The Object-Oriented Python and Types course established encapsulation as a **contract at the
name level**; visibility there was an agreement, and is not repeated here. The distinction
here is structural: the decision is made by the language's own compiler, and rejection is not
a warning, it is a source that does not compile.

## The Measurement Core

The measurement runs no program at all. For every pair, a source set is produced, the compiler
is invoked from within the program, and only the exit status is read.

- **CI7** — The measurement's oracle is the rig itself: in every scenario the member is
  declared in exactly one place and read from exactly one place; there is no other reason for
  a compile failure.
- **CI8** — Every message the compiler produces is swallowed; the only thing measured is exit
  status. Why a source was rejected is read from the rig, not from the text.
- **CI9** — The same member is used in every scenario: `Base`'s `label` field. The only thing
  that changes across levels is that field's modifier.

```java
// Compile.java — compiles a source set; the result is only compiled or not compiled
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.reflect.AccessFlag;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Compile {
    static boolean compiles(Map<String, String> files) throws Exception {
        Path d = Files.createTempDirectory("access");
        List<String> arg = new ArrayList<>(List.of("-d", d.resolve("out").toString()));
        for (var g : files.entrySet()) {
            Path p = d.resolve(g.getKey());
            Files.createDirectories(p.getParent());
            Files.writeString(p, g.getValue());
            arg.add(p.toString());
        }
        PrintWriter sink = new PrintWriter(Writer.nullWriter());
        return ToolProvider.findFirst("javac").orElseThrow()
                .run(sink, sink, arg.toArray(String[]::new)) == 0;
    }

    static List<String> fieldFlags(String source, String className) throws Exception {
        Path d = Files.createTempDirectory("flags");
        Path f = d.resolve(className + ".java");
        Files.writeString(f, source);
        PrintWriter sink = new PrintWriter(Writer.nullWriter());
        if (ToolProvider.findFirst("javac").orElseThrow()
                .run(sink, sink, "-d", d.toString(), f.toString()) != 0)
            throw new IllegalStateException("did not compile: " + className);
        List<String> lines = new ArrayList<>();
        for (FieldModel fm : ClassFile.of().parse(d.resolve(className + ".class")).fields()) {
            var flags = fm.flags();
            lines.add(fm.fieldName().stringValue() + " " + (flags.has(AccessFlag.PRIVATE) ? "ACC_PRIVATE"
                    : flags.has(AccessFlag.PROTECTED) ? "ACC_PROTECTED"
                    : flags.has(AccessFlag.PUBLIC) ? "ACC_PUBLIC" : "(no flag)"));
        }
        return lines;
    }
}
```

## How Many of Sixteen Pairs Compile

```java
// Access.java — four visibility levels x four call positions
import java.util.*;

public class Access {
    static final String[] LEVELS = { "private", "", "protected", "public" };
    static final String[] LEVEL_NAMES = { "private", "(default)", "protected", "public" };
    static final String FORMAT = "%-13s %-13s %-13s %-13s %s%n";

    static String base(String level) {
        return "package home;\npublic class Base {\n"
                + "    " + level + " String label = \"base field\";\n"
                + "    public String own() { return label; }\n}\n";
    }

    static Map<String, String> scenario(String level, int position) {
        Map<String, String> d = new LinkedHashMap<>();
        d.put("home/Base.java", base(level));
        switch (position) {
            case 1 -> d.put("home/Neighbor.java",
                    "package home;\nclass Neighbor { String read(Base u) { return u.label; } }\n");
            case 2 -> d.put("away/OutsideSub.java", "package away;\nimport home.Base;\n"
                    + "public class OutsideSub extends Base { public String read() { return this.label; } }\n");
            case 3 -> d.put("away/Outsider.java", "package away;\nimport home.Base;\n"
                    + "class Outsider { String read(Base u) { return u.label; } }\n");
            default -> { }
        }
        return d;
    }

    public static void main(String[] args) throws Exception {
        System.out.printf(FORMAT, "level", "same class", "same package", "subclass", "outside pkg");
        int compiledCount = 0;
        for (int i = 0; i < LEVELS.length; i++) {
            String[] results = new String[4];
            for (int j = 0; j < 4; j++) {
                boolean ok = Compile.compiles(scenario(LEVELS[i], j));
                results[j] = ok ? "compiled" : "not compiled";
                if (ok) compiledCount++;
            }
            System.out.printf(FORMAT, LEVEL_NAMES[i], results[0], results[1], results[2], results[3]);
        }
        System.out.println("compiled count of 16 pairs: " + compiledCount);
    }
}
```

```
level         same class    same package  subclass      outside pkg
private       compiled      not compiled  not compiled  not compiled
(default)     compiled      compiled      not compiled  not compiled
protected     compiled      compiled      compiled      not compiled
public        compiled      compiled      compiled      compiled
compiled count of 16 pairs: 10
```

**Ten** of sixteen pairs compile, six do not. The table's shape is a staircase: every row opens
what the previous one opened and adds one more position on top. Levels, for this reason, are
not four separate rules, they are **four nested rings**; moving a member from one level to the
next opens a new door and closes none.

The first column is the same across all four rows. A class sees its own member at every level;
even `private` closes nothing inside the class itself. Visibility does not hide a member from
its owner, it bounds **the calling side**.

The only difference between the first and second row is the absence of a single word, and that
absence lets in every class in the same package. An unmarked field can be read from the
`Neighbor` class; `Neighbor` carries no inheritance relationship to `Base` at all, it merely
sits in the same package. In this table, a package is not an organizational unit, it is an
**access boundary**.

What the last row says is that the fourth column fills for the first time. `public` is the
only level opening outside the package declaring the member; the remaining three are bound to
the package to some degree or other.

All of these decisions are made in one single place: the compiler. In none of the sixteen
scenarios did a program run. For six of the pairs there is not even a class file — the defect
shows up not as an error in a running program, but as **a program that never came to exist**.
Among the eight decisions this course measures, visibility is the extreme where the runtime
type's share is zero.

## A Bounding Measurement: Protected Level Also Opens the Package

One row in the table resists the first reading. `protected` is mostly read as "open only to
subclasses"; yet the third row's second column says **compiled**. A class that is not a
subclass, and merely sits in the same package, can read the protected field.

This single measurement does more than bound a rule — it also reshapes what `protected` is.
To see the detail, the same protected field is tried from five separate places.

- **CI10** — In all five trials, the `Base` class and the `label` field are byte-for-byte
  identical; what changes is only the trying class's package, its inheritance relationship,
  and the **declared type of the reference** it uses.

```java
// Protection.java — what protected level actually opens
import java.util.*;

public class Protection {
    static final String BASE = "package home;\npublic class Base {\n"
            + "    protected String label = \"base field\";\n}\n";
    static final String PREFIX = "package away;\nimport home.Base;\n";

    static final String[][] TRIALS = {
        { "same package, unrelated class", "home/Neighbor.java",
          "package home;\nclass Neighbor { String read(Base u) { return u.label; } }\n" },
        { "outside package, subclass, via this", "away/A.java",
          PREFIX + "public class A extends Base { String read() { return this.label; } }\n" },
        { "outside package, subclass, via own type", "away/B.java",
          PREFIX + "public class B extends Base { String read(B b) { return b.label; } }\n" },
        { "outside package, subclass, via Base type", "away/C.java",
          PREFIX + "public class C extends Base { String read(Base u) { return u.label; } }\n" },
        { "outside package, unrelated class", "away/D.java",
          PREFIX + "class D { String read(Base u) { return u.label; } }\n" },
    };

    public static void main(String[] args) throws Exception {
        System.out.printf("%-44s %s%n", "access site", "result");
        for (String[] d : TRIALS) {
            Map<String, String> k = new LinkedHashMap<>();
            k.put("home/Base.java", BASE);
            k.put(d[1], d[2]);
            System.out.printf("%-44s %s%n", d[0], Compile.compiles(k) ? "compiled" : "not compiled");
        }
    }
}
```

```
access site                                  result
same package, unrelated class                compiled
outside package, subclass, via this          compiled
outside package, subclass, via own type      compiled
outside package, subclass, via Base type     not compiled
outside package, unrelated class             not compiled
```

**Three** of five trials compile. The first row is the bounding measurement itself: a
protected member is open to the **whole package**, and inheritance is not required for that.
The sentence "protected means open only to subclasses" is disproved by the measurement.

The difference between the third and fourth rows is the most overlooked side of
`protected`. Both classes are `Base`'s subclass, both sit in an outside package, both try to
read the same field. The distinction is in **which reference the read is made through**: `B`
uses a reference of its own type and compiles, `C` uses a reference whose declared type is
`Base` and does not compile. The object can be the same object; the decision is made by
looking not at the object, but at the reference's **declared type**.

This is the lesson's opening claim in its sharpest form. Same field, same subclass, same line
shape — and two separate outcomes. The only thing producing the difference is the type the
compiler sees while resolving that name. A subclass in an outside package can use a protected
member **through its own line of inheritance**, not as a general tool of the base type.

## Where the Member Sits

Do accesses rejected at compile time have any runtime counterpart at all? The question is
answered by looking at what state the member is in inside the class file.

```java
// Flag.java — what four levels leave in the class file
public class Flag {
    static final String SOURCE = """
        class Member {
            private   String hidden  = "";
                      String shared  = "";
            protected String guarded = "";
            public    String open    = "";
        }
        """;

    public static void main(String[] args) throws Exception {
        var fields = Compile.fieldFlags(SOURCE, "Member");
        System.out.println("field count in class file: " + fields.size());
        for (String s : fields) System.out.println("  " + s);
    }
}
```

```
field count in class file: 4
  hidden ACC_PRIVATE
  shared (no flag)
  guarded ACC_PROTECTED
  open ACC_PUBLIC
```

All four of the four fields are in the class file. The `private`-declared field has not been
deleted, hidden, or moved anywhere else; it sits in the same list, in the same form, as the
others. The only thing separating them is a **flag**.

Package level's counterpart shows here too. Each of the other three levels has a name in the
class file; the unmarked field carries no flag at all. The namelessness in the source stays
nameless in the class file too: package level is the absence of any of the three flags being
set.

This is where the lesson's second half comes from. Visibility is not a **hiding mechanism**;
it does not put a member where no one can reach it. What it does is tell the compiler which
sources to reject. Encapsulation is not a barrier, it is a contract — and that contract's
enforcement shows up not in a running program, but in a program that never gets born at all.

## In Whose Compile Does the Defect Show

Narrowing a level is changing a single word in its owner's source. Who this change hits can be
measured: the same `Base` class is compiled at two levels, first alone, then together with a
caller in an outside package.

```java
// Cost.java — when a level is narrowed, whose compile does the defect break
import java.util.*;

public class Cost {
    static final String CALLER = "package away;\nimport home.Base;\n"
            + "class Outsider { String read(Base u) { return u.label; } }\n";

    static String base(String level) {
        return "package home;\npublic class Base {\n    " + level
                + " String label = \"base field\";\n}\n";
    }

    public static void main(String[] args) throws Exception {
        for (String level : new String[] { "public", "protected" }) {
            Map<String, String> both = new LinkedHashMap<>();
            both.put("home/Base.java", base(level));
            both.put("away/Outsider.java", CALLER);
            System.out.printf("%-10s | Base alone: %-13s | with caller: %s%n", level,
                    Compile.compiles(Map.of("home/Base.java", base(level))) ? "compiled" : "not compiled",
                    Compile.compiles(both) ? "compiled" : "not compiled");
        }
    }
}
```

```
public     | Base alone: compiled      | with caller: compiled
protected  | Base alone: compiled      | with caller: not compiled
```

The middle column says **compiled** on both rows. `Base`'s source is flawless at protected
level too; it compiles on its own, a class file is produced, no warning is raised. Nothing
visible shows on the owner's side.

The right column, though, splits. The same `Outsider` class, the same line, two separate
outcomes — and nothing changed in `Outsider`'s own source. The defect is born not in the
compile of the class that made the change, but in **the compile of the class looking at it**.

This is this lesson's answer to the "where does the cost show" question every lesson in this
course asks. Widening visibility is retroactively safe: it opens a new door and closes none
that were open. Narrowing produces a break somewhere the owner cannot see. The cost of
declaring a member `public` is the cost of later taking that back.

## Summary

- Crossing four visibility levels with four call positions, **ten** of sixteen pairs compile;
  the table is shaped like four nested rings, each level adding one position atop the last.
- Visibility does not hide a member from its owner — the first column compiles at all four
  levels — what is bounded is **the calling side**; the unmarked level is not a non-choice
  either, it lets in unrelated classes in the same package and turns the package into an
  access boundary.
- Protected level also opens up the package: a neighbor class that is not a subclass can read
  a protected field, so "open only to subclasses" is the wrong reading.
- A subclass in an outside package can reach a protected member through a reference **of its
  own type**, not through a reference whose declared type is the base class: the decision
  looks not at the object, but at the reference's declared type.
- All four of the four fields sit in the class file; only a flag carries the distinction, and
  package level carries no flag. Visibility is not hiding, it is a rule handed to the compiler.
- When a level is narrowed, the owner's source still compiles; the defect is born in **the
  caller's compile**. Widening is retroactively safe, narrowing breaks somewhere else.

## Next Step

For two lessons the same pair has been used, and each measured one side of the decision: the
runtime type chose the method called from the constructor, the declared type decided
accessibility. The two were never set side by side. The next lesson is this course's core
measurement: eight separate decisions are read on a single reference, and how many of them
came from which side is counted. On the same line, `d.name()` and `d.label` give two opposing
answers — redeclaring fields under the same name is not overriding, it is **hiding**, and a
hidden field does not vanish. In the same lesson, what the `@Override` mark catches at compile
time, and why the decision switches sides when a signature changes, are measured.
