Javaのアクセス修飾子とは、クラスやメンバー(フィールド・メソッド・コンストラクタ)を「どこから使えるか」を決めるキーワードで、広い順に public・protected・修飾子なし(パッケージプライベート)・private の4段階です。 private は「そのクラスの中だけ」、protected は「同じパッケージ+サブクラス」から使えます。

この記事では、まず4修飾子×4つのアクセス元の一覧表で答えを示し、そのあとパッケージを2つに分けた実際のファイル構成でjavacを実行し、出てきたエラー文を全文で掲載します。「別パッケージのサブクラスなのに protected が使えない」という落とし穴の再現、クラス・インターフェース・record・enum・コンストラクタで何も書かなかったときに付く既定の修飾子をリフレクションで確かめた結果、オーバーライドで修飾子を狭めたときのエラー、そして「まず private から考える」判断フローとモジュール(module-info の exports)まで、1ページでまとめています。コードはすべて OpenJDK 21.0.10 で実行し、Java 25 での差分は公式ドキュメントで確認して注記しました。

Javaアクセス修飾子の早見表

まずはこの表で答えを確認してください。 ○はアクセスできる、×はコンパイルエラーになる、を表します。

修飾子 同じクラス 同じパッケージ 別パッケージのサブクラス 別パッケージ(継承なし)
public ○ ○ ○ ○
protected ○ ○ ○(※自分の型経由のみ) ×
なし(パッケージプライベート) ○ ○ × ×
private ○ × × ×

※ 別パッケージのサブクラスから protected メンバーを使えるのは、this や自分自身の型(またはそのサブ型)の参照を通したときだけです。親の型の変数を経由すると×になります(後半で再現します)。

どこに付けられるかも一緒に押さえると迷いません。

付ける場所 public protected なし private
トップレベルのクラス・インターフェース・enum・record ○ × ○ ×
ネストしたクラス(クラスの中のクラス) ○ ○ ○ ○
フィールド・メソッド・コンストラクタ(クラス内) ○ ○ ○ ○
インターフェースのメソッド ○(省略時も public) × ― ○(Java 9以降、本体ありのみ)
インターフェースのフィールド ○(常に public static final) × ― ×
enum のコンストラクタ × × ○(常に private 扱い) ○
ローカル変数・メソッドの引数 × × ― ×

エラー文の早見表(Java 21.0.10、英語ロケール/日本語ロケールで実際に出たもの)です。

原因 英語のエラー文 日本語のエラー文
private を外から使った memo has private access in Item memoはItemでprivateアクセスされます
修飾子なしを別パッケージから使った price is not public in Item; cannot be accessed from outside package Itemのpriceはpublicではありません。パッケージ外からはアクセスできません
protected を関係のないクラスから使った stock has protected access in Item stockはItemでprotectedアクセスされます
修飾子なしのクラスを別パッケージから使った Hidden is not public in com.example.shop; cannot be accessed from outside package ―
オーバーライドで狭めた attempting to assign weaker access privileges; was public ―
exports されていないパッケージを使った package com.example.shop.internal is not visible ―

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Javaアクセス修飾子一覧|privateとprotectedの違いを表とエラー例で」

4つのアクセス修飾子をひとことで

4つの違いは「公開する範囲の広さ」だけです。 意味をひとことでまとめると次のとおりです。

修飾子 ひとことで 主な使いどころ
public どこからでも使える 外部に約束するAPI(公開メソッド、公開クラス)
protected 同じパッケージ+継承した子クラス 子クラスに上書き・利用してほしいフック(テンプレートメソッドなど)
なし(パッケージプライベート) 同じパッケージの中だけ パッケージ内の部品、テスト用に同じパッケージから触りたいもの
private 宣言したクラスの中だけ フィールド、内部処理の補助メソッド

用語を2つだけ確認しておきます。

  • パッケージ:クラスをまとめるフォルダのような単位です。ファイル先頭の package com.example.shop; で決まります。
  • パッケージプライベート:修飾子を何も書かない状態の通称です。英語では package-private や default access と呼ばれます。default というキーワードを書くわけではありません(default はインターフェースのデフォルトメソッドや switch で使う別の意味のキーワードです)。

よくある誤解は「protected は private より少し広いだけ」というものです。 実際は、protected は修飾子なしよりも広い範囲です。同じパッケージからは修飾子なしと同じく使え、さらに別パッケージのサブクラスからも使えます。並べると private < なし < protected < public の順になります。

パッケージを分けたサンプルで動きを確かめる

アクセス修飾子の違いは、パッケージが2つ以上あるときに初めてはっきり見えます。 1ファイルだけの例では「同じパッケージ」と「別パッケージ」の差が出ないため、ここではフォルダを分けた構成で試します。

ファイル構成

src
└── com
    └── example
        ├── app                 ← 別パッケージ
        │   ├── Main.java       (継承なし)
        │   └── SaleItem.java   (Item を継承したサブクラス)
        └── shop                ← Item と同じパッケージ
            ├── Counter.java    (Item を継承しない別クラス)
            └── Item.java       (4種類の修飾子を持つクラス)

コード(4ファイル)

// src/com/example/shop/Item.java
package com.example.shop;

public class Item {
    public String name = "ペン";          // どこからでも
    protected int stock = 10;             // 同じパッケージ + サブクラス
    int price = 120;                      // 同じパッケージだけ(修飾子なし)
    private String memo = "仕入れメモ";   // このクラスだけ

    public String describe() {
        // 同じクラスの中なら private も含め全部使える
        return name + " / 在庫" + stock + " / " + price + "円 / " + memo;
    }
}
// src/com/example/shop/Counter.java
package com.example.shop;

// 同じパッケージの別クラス(サブクラスではない)
public class Counter {
    public static void check() {
        Item item = new Item();
        System.out.println("name="  + item.name);   // OK
        System.out.println("stock=" + item.stock);  // OK(同じパッケージ)
        System.out.println("price=" + item.price);  // OK(同じパッケージ)
        // System.out.println(item.memo);           // NG:private
    }
}
// src/com/example/app/SaleItem.java
package com.example.app;

import com.example.shop.Item;

// 別パッケージのサブクラス
public class SaleItem extends Item {
    public void check() {
        System.out.println("name="  + name);   // OK
        System.out.println("stock=" + stock);  // OK(自分が継承した protected)
        // System.out.println(price);          // NG:修飾子なし
        // System.out.println(memo);           // NG:private
    }
}
// src/com/example/app/Main.java
package com.example.app;

import com.example.shop.Counter;
import com.example.shop.Item;

// 別パッケージ・継承なし
public class Main {
    public static void main(String[] args) {
        Item item = new Item();
        System.out.println(item.describe());
        Counter.check();
        new SaleItem().check();
        System.out.println("name=" + item.name);  // OK:public だけ見える
    }
}

コンパイルと実行

# src の親フォルダで実行(Java 21.0.10)
javac -encoding UTF-8 -d out $(find src -name '*.java')
java -cp out com.example.app.Main

実行結果(OpenJDK 21.0.10):

ペン / 在庫10 / 120円 / 仕入れメモ
name=ペン
stock=10
price=120
name=ペン
stock=10
name=ペン

出力から読み取れるポイントは次の3つです。

  • describe() は Item 自身のメソッドなので、private の memo も含めて全部使えています。
  • Counter(同じパッケージ)は stock と price まで使えます。修飾子なしと protected は、同じパッケージからは同じ扱いです。
  • SaleItem(別パッケージのサブクラス)は name と stock だけです。price(修飾子なし)は継承していても使えません。

アクセスできないときのエラー文を全部見る

コメントアウトしていた行を有効にすると、どんなエラーになるのかを1つずつ確かめます。 エラーになるファイルは ng フォルダに分け、先ほどの out をクラスパスに指定してコンパイルしました。

javac -encoding UTF-8 -cp out -d ngout ng/com/example/shop/NgPrivate.java

private を別クラスから使う:has private access

// ng/com/example/shop/NgPrivate.java(Item と同じパッケージ)
package com.example.shop;

public class NgPrivate {
    void run() {
        Item item = new Item();
        System.out.println(item.memo);
    }
}
ng/com/example/shop/NgPrivate.java:6: error: memo has private access in Item
        System.out.println(item.memo);
                               ^
1 error

同じパッケージでも private は使えません。 private の境界は「パッケージ」ではなく「クラス(正確にはトップレベルクラスの本体)」です。

修飾子なしを別パッケージから使う:is not public

// ng/com/example/app/NgPackage.java
package com.example.app;

import com.example.shop.Item;

public class NgPackage {
    void run() {
        Item item = new Item();
        System.out.println(item.price);
    }
}
ng/com/example/app/NgPackage.java:8: error: price is not public in Item; cannot be accessed from outside package
        System.out.println(item.price);
                               ^
1 error

「is not public」と出たら、修飾子なし(パッケージプライベート)を別パッケージから触っている合図です。 直し方は、①呼び出す側を同じパッケージに移す、②公開してよいなら public のメソッドを用意する、のどちらかです。

protected を関係のないクラスから使う:has protected access

// ng/com/example/app/NgProtected.java(Item を継承していない)
package com.example.app;

import com.example.shop.Item;

public class NgProtected {
    void run() {
        Item item = new Item();
        System.out.println(item.stock);
    }
}
ng/com/example/app/NgProtected.java:8: error: stock has protected access in Item
        System.out.println(item.stock);
                               ^
1 error

修飾子なしのクラスを別パッケージから使う

クラスそのものに public を付けないと、import の行の時点でエラーになります。

// src/com/example/shop/Hidden.java
package com.example.shop;

class Hidden {          // public なし=パッケージ内だけ
    public void hello() { System.out.println("hello"); }
}
// ng/com/example/app/NgClass.java
package com.example.app;

import com.example.shop.Hidden;

public class NgClass {
    void run() { new Hidden().hello(); }
}
ng/com/example/app/NgClass.java:3: error: Hidden is not public in com.example.shop; cannot be accessed from outside package
import com.example.shop.Hidden;
                       ^
ng/com/example/app/NgClass.java:6: error: Hidden is not public in com.example.shop; cannot be accessed from outside package
    void run() { new Hidden().hello(); }
                     ^
2 errors

メソッドに public が付いていても、クラスが見えなければ使えません。 アクセスできるかどうかは「クラスが見えるか」→「メンバーが見えるか」の2段階で決まります。

日本語環境で出るエラー文

OSのロケールが日本語の場合、javac のメッセージは日本語になります。-J-Duser.language=ja を付けて同じファイルをコンパイルした結果(各1行目)です。

ng/com/example/shop/NgPrivate.java:6: エラー: memoはItemでprivateアクセスされます
ng/com/example/app/NgPackage.java:8: エラー: Itemのpriceはpublicではありません。パッケージ外からはアクセスできません
ng/com/example/app/NgParentRef.java:9: エラー: stockはItemでprotectedアクセスされます

検索するときは英語のエラー文のほうが情報が多いので、-J-Duser.language=en を付けて英語で出し直すのも手です。

エラー文(英語) 意味 まず確認すること
X has private access in Y private を Y の外から使った getter などの公開メソッドがあるか
X is not public in Y; cannot be accessed from outside package 修飾子なしを別パッケージから使った 呼び出し側のパッケージ名、公開してよい部品か
X has protected access in Y protected を継承関係のないクラス、または親型の参照経由で使った 継承しているか、this/自分の型経由か
attempting to assign weaker access privileges; was public オーバーライドで修飾子を狭めた 親と同じかそれより広い修飾子にする
modifier private not allowed here その場所に付けられない修飾子を付けた 上の「どこに付けられるか」表

privateとprotectedの違い

private は「自分のクラスだけ」、protected は「同じパッケージ+子クラス」です。 違いを観点別に並べます。

観点 private protected
同じクラス ○ ○
同じパッケージの別クラス × ○
別パッケージのサブクラス × ○(自分の型経由のみ)
別パッケージ(継承なし) × ×
サブクラスに継承されるか されない(見えない) される
オーバーライドできるか できない(同名メソッドは別物になる) できる(public に広げてもよい)
主な目的 内部の状態を隠す(カプセル化) 子クラスに拡張ポイントを渡す
トップレベルクラスに付けられるか × ×

「カプセル化」とは、データとそれを扱う処理を1つのクラスにまとめ、外から直接触れないようにする考え方です。 private はその中心になる修飾子です。

protected の落とし穴:親の型の参照経由ではアクセスできない

別パッケージのサブクラスでも、親の型(Item)の変数を通すと protected メンバーは使えません。 Java Silver などの試験でもよく問われる仕様です。

// ng/com/example/app/NgParentRef.java
package com.example.app;

import com.example.shop.Item;

public class NgParentRef extends Item {
    void compare(Item other, NgParentRef same) {
        System.out.println(this.stock);   // OK:自分自身
        System.out.println(same.stock);   // OK:自分の型(またはそのサブ型)経由
        System.out.println(other.stock);  // NG:親の型 Item 経由
    }
}
ng/com/example/app/NgParentRef.java:9: error: stock has protected access in Item
        System.out.println(other.stock);  // NG:親の型 Item 経由
                                ^
1 error

エラーは other.stock の1か所だけで、this.stock と same.stock は通っています。

なぜこうなるのかは、言語仕様(JLS §6.6.2.1)に書かれています。要約すると「別パッケージのサブクラス S から protected メンバーを 式.メンバー で使えるのは、式の型が S か S のサブクラスのときだけ」です。

理由をイメージで説明すると、Item other には、SaleItem や別の誰かが作った別のサブクラスのインスタンスが入っている可能性があります。protected は「自分の家系(自分の型)の実装に責任を持つ子クラス」に開かれた扉なので、よその家系のオブジェクトの内部までは触れない、という設計です。

直し方は3通りあります。

状況 直し方
自分と同じ型だけ扱えばよい 引数の型を自分の型(NgParentRef same)にする
親の型のまま値だけ読みたい 親クラスに public の getter(getStock())を用意する
パッケージ内の部品どうしで共有したい 使う側を親と同じパッケージに置く(同じパッケージなら親型経由でも○)

Object.clone() が呼べないのも同じ理由

Object の clone() は protected です。 そのため、別パッケージ(java.lang 以外)のクラスから Object 型の変数に対して呼ぶとエラーになります。

// ng/com/example/app/NgClone.java
package com.example.app;

public class NgClone {
    void run(Object o) {
        Object copy = o.clone();
    }
}
ng/com/example/app/NgClone.java:5: error: clone() has protected access in Object
        Object copy = o.clone();
                       ^
1 error

すべてのクラスは Object のサブクラスですが、o は「Object 型の参照」なので、上の落とし穴と同じルールが当てはまります。コピーを提供したいクラスは、clone を public でオーバーライドするか、コピーコンストラクタや copy() メソッドを用意するのが一般的です。

protected コンストラクタは「継承してね」の合図

コンストラクタに protected を付けると、別パッケージからは new クラス名() で直接作れず、サブクラス経由でだけ作れます。

// src/com/example/shop/Template.java
package com.example.shop;

public abstract class Template {
    protected Template() { System.out.println("Template 生成"); }
    public abstract String title();
}
// src/com/example/shop/Box.java
package com.example.shop;

public class Box {
    protected Box() { }
}
// src/com/example/app/UseCtor.java(直す前)
package com.example.app;

import com.example.shop.Box;
import com.example.shop.Template;

public class UseCtor {
    public static void main(String[] args) {
        Template t = new Template() {          // 匿名サブクラス経由なら OK
            public String title() { return "週報"; }
        };
        System.out.println(t.title());
        Box b = new Box();                     // NG:protected コンストラクタを直接
    }
}
src/com/example/app/UseCtor.java:12: error: Box() has protected access in Box
        Box b = new Box();                     // NG:protected コンストラクタを直接
                ^
1 error

new Box() を new Box() { }(中身が空の匿名サブクラス)に直すとコンパイルが通り、実行結果は次のとおりです。

Template 生成
週報

抽象クラスのコンストラクタは protected にしておくと、「継承して使うクラス」という意図がコードから読み取れるようになります。

何も書かないとどうなる?既定の修飾子一覧

修飾子を省略したときの扱いは、書く場所によって変わります。 クラスなら「パッケージプライベート」ですが、インターフェースの中では「public」、enum のコンストラクタでは「private」です。リフレクション(実行時にクラスの情報を調べる仕組み)で実際の修飾子を表示して確かめました。

// DefaultModifiers.java(java DefaultModifiers.java で単一ファイル実行)
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

public class DefaultModifiers {

    // 調べる対象(すべて何も修飾子を書いていない)
    public static class PublicTask { }          // public クラスの既定コンストラクタ
    static class PackageTask { }                // 修飾子なしクラスの既定コンストラクタ
    enum Status { TODO, DONE }                  // enum のコンストラクタ
    record Member(String name, int age) { }     // record のフィールド・アクセサ・コンストラクタ
    interface Shape {
        int SIDES = 0;                          // インターフェースの定数
        double area();                          // 抽象メソッド
        default String label() { return "shape"; }
        static Shape unit() { return () -> 1.0; }
        private String secret() { return "x"; } // Java 9+ の private メソッド
        class Helper { }                        // インターフェース内のネスト型
    }

    // ネストしたクラスの private は外側から使える
    static class Wallet {
        private int coins = 3;
    }

    static String mod(int m) {
        String s = Modifier.toString(m);
        return s.isEmpty() ? "(なし=パッケージプライベート)" : s;
    }

    public static void main(String[] args) throws Exception {
        System.out.println("Java " + Runtime.version());
        for (Class<?> c : new Class<?>[]{PublicTask.class, PackageTask.class, Status.class, Member.class}) {
            for (Constructor<?> k : c.getDeclaredConstructors()) {
                System.out.printf("%-12s コンストラクタ: %s%n", c.getSimpleName(), mod(k.getModifiers()));
            }
        }
        for (Field f : Member.class.getDeclaredFields()) {
            System.out.printf("Member.%-5s フィールド: %s%n", f.getName(), mod(f.getModifiers()));
        }
        for (String n : new String[]{"name", "age"}) {
            Method m = Member.class.getDeclaredMethod(n);
            System.out.printf("Member.%s() アクセサ: %s%n", n, mod(m.getModifiers()));
        }
        System.out.println("Shape.SIDES: " + mod(Shape.class.getDeclaredField("SIDES").getModifiers()));
        for (String n : new String[]{"area", "label", "unit", "secret"}) {
            Method m = Shape.class.getDeclaredMethod(n);
            System.out.printf("Shape.%s(): %s%n", n, mod(m.getModifiers()));
        }
        System.out.println("Shape.Helper: " + mod(Shape.Helper.class.getModifiers()));
        System.out.println("Wallet.coins を外側から読む: " + new Wallet().coins);
    }
}

実行結果(java -Dstdout.encoding=UTF-8 DefaultModifiers.java、OpenJDK 21.0.10):

Java 21.0.10+7-Ubuntu-124.04
PublicTask   コンストラクタ: public
PackageTask  コンストラクタ: (なし=パッケージプライベート)
Status       コンストラクタ: private
Member       コンストラクタ: (なし=パッケージプライベート)
Member.name  フィールド: private final
Member.age   フィールド: private final
Member.name() アクセサ: public
Member.age() アクセサ: public
Shape.SIDES: public static final
Shape.area(): public abstract
Shape.label(): public
Shape.unit(): public static
Shape.secret(): private
Shape.Helper: public static
Wallet.coins を外側から読む: 3

結果を表にまとめると、次のようになります。

宣言 何も書かないときの扱い 補足
トップレベルのクラス パッケージプライベート 付けられるのは public か「なし」だけ
既定コンストラクタ(自動で作られるもの) クラスと同じ修飾子 public クラスなら public、なしなら なし
クラスのフィールド・メソッド パッケージプライベート ―
インターフェースの抽象メソッド public abstract 実装クラス側では public が必須
インターフェースの default/static メソッド public ―
インターフェースの private メソッド private(明示が必要) Java 9 以降。本体が必要
インターフェースのフィールド public static final 定数になる
インターフェース内のネスト型 public static ―
enum のコンストラクタ private public や protected は付けられない
record のフィールド private final 書き換え不可
record のアクセサ(name()) public 自分で書くときも public 必須
record の正規コンストラクタ(自動) record と同じ修飾子 自分で書くときは record 以上の広さが必須

付けられない場所に付けたときのエラー

「どこに付けられるか」の表で×の組み合わせは、すべてコンパイルエラーになります。 実際のエラー文です。

// NgTop.java
private class NgTop { }
NgTop.java:1: error: modifier private not allowed here
private class NgTop { }
        ^
1 error
// NgEnum.java
enum Size {
    S, M, L;
    public Size() { }
}
NgEnum.java:3: error: modifier public not allowed here
    public Size() { }
           ^
1 error
// NgInterfaceField.java
interface Limits {
    private int MAX = 10;
}
NgInterfaceField.java:2: error: modifier private not allowed here
    private int MAX = 10;
                ^
1 error

record では、自分で書いた正規コンストラクタ(コンパクトコンストラクタ)やアクセサの修飾子にもルールがあります。

// NgRecord.java
public record NgRecord(int x) {
    NgRecord {                 // record より狭いアクセス
    }
}
NgRecord.java:2: error: invalid canonical constructor in record NgRecord
    NgRecord {                 // record より狭いアクセス
    ^
  (attempting to assign stronger access privileges; was public)
1 error
// NgAccessor.java
record Point(int x, int y) {
    int x() { return x; }      // アクセサを public にし忘れ
}
NgAccessor.java:2: error: invalid accessor method in record Point
    int x() { return x; }      // アクセサを public にし忘れ
        ^
  (accessor method must be public)
1 error

record のアクセサは public、正規コンストラクタは record と同じかそれより広く、と覚えておけば迷いません。

オーバーライドではアクセスを狭められない

メソッドをオーバーライド(親のメソッドを子で上書き)するとき、修飾子は親と同じか、それより広くしかできません。 親を public にしたメソッドを子で protected にすると、次のエラーになります。

// NgOverride.java
class Base {
    public void show() { }
}
class Child extends Base {
    protected void show() { }   // public → protected に狭める
}
NgOverride.java:5: error: show() in Child cannot override show() in Base
    protected void show() { }   // public → protected に狭める
                   ^
  attempting to assign weaker access privileges; was public
1 error

インターフェースを実装するときに public を書き忘れるのも、同じエラーです。 インターフェースのメソッドは省略しても public なので、実装側で修飾子なしにすると「狭めた」ことになります。

// NgInterface.java
interface Printable {
    void print();               // 書かなくても public abstract
}
class Report implements Printable {
    void print() { }            // public を付け忘れ
}
NgInterface.java:5: error: print() in Report cannot implement print() in Printable
    void print() { }            // public を付け忘れ
         ^
  attempting to assign weaker access privileges; was public
1 error

なぜ狭められないのかは、ポリモーフィズム(親の型の変数で子の実装を呼べる仕組み)を考えるとわかります。Base b = new Child(); b.show(); は、Base の show が public なのでどこからでも書けます。子で狭められると、この呼び出しが成り立たなくなるため、言語として禁止されています。

親の修飾子 子で許される修飾子
public public のみ
protected protected、public
なし なし、protected、public(同じパッケージにある場合のみオーバーライドになる)
private オーバーライドされない(同名メソッドを書いても別のメソッド)

private メソッドはオーバーライドの対象外です。子クラスに同じ名前のメソッドを書いても、それは新しい別のメソッドになり、@Override を付けるとコンパイルエラーで気づけます。

ネストしたクラスと private

クラスの中に書いたクラス(ネストしたクラス)どうしは、お互いの private を使えます。 先ほどの実行結果の最終行 Wallet.coins を外側から読む: 3 がその例です。private の境界は「トップレベルクラスの本体全体」なので、外側のクラスと内側のクラスは同じ範囲として扱われます。

アクセスする側 → される側 private を使えるか
外側のクラス → 内側(ネスト)クラスの private ○
内側クラス → 外側クラスの private ○
ネストクラスどうし(同じトップレベルクラス内) ○
別のトップレベルクラス(同じファイルでも) ×

Java 11 以降は、この仕組みが JVM レベルで「ネストメイト(nestmates)」として扱われ、コンパイラが補助メソッドを作らなくても直接アクセスできるようになっています。書き方や動きは変わらないので、「同じトップレベルクラスの中なら private も見える」とだけ覚えておけば十分です。

どれを選ぶか:「まず private」から考える判断フロー

迷ったら、いちばん狭い private から始めて、必要になった分だけ広げるのが基本です。狭いものを後から広げるのは簡単ですが、一度 public にして他のコードから使われたものを狭めるのは、使っている側をすべて直す必要があるからです。

flowchart TD
    A[メンバーを追加する] --> B{このクラスの外から使う?}
    B -- いいえ --> P[private]
    B -- はい --> C{同じパッケージの中だけで足りる?}
    C -- はい --> D[修飾子なし]
    C -- いいえ --> E{別パッケージの子クラスに<br/>上書き・利用してほしい?}
    E -- はい --> F[protected]
    E -- いいえ --> G{外部に約束するAPI?}
    G -- はい --> H[public]
    G -- いいえ --> I[設計を見直す<br/>公開メソッドを1つにまとめる等]

種類ごとのおすすめを表にしました。

対象 おすすめ 理由
フィールド private(定数は public static final も可) 値の変更をメソッドに集めて、チェックを入れられるようにする
内部処理の補助メソッド private 呼び出し元を限定し、変更しやすくする
パッケージ内だけで使うクラス 修飾子なし 外部に見せる必要がない部品を隠せる
テストから呼びたい内部メソッド 修飾子なし(テストを同じパッケージに置く) public にせずにテストできる
子クラスに上書きしてほしいメソッド protected 外部には見せず、拡張だけ許可できる
抽象クラスのコンストラクタ protected 継承前提であることを示せる
外部に提供する操作 public APIとして約束する

よくある直し方も1つ紹介します。フィールドを public にしている場合は、private にして必要なメソッドだけ公開します。

// 直す前:どこからでも在庫数を書き換えられる
public class Stock {
    public int count;
}
// 直した後:変更はメソッド経由にして、チェックを1か所に集める
public class Stock {
    private int count;

    public int count() {
        return count;
    }

    public void add(int n) {
        if (n <= 0) {
            throw new IllegalArgumentException("n は1以上: " + n);
        }
        count += n;
    }
}

値を持つだけのクラスなら record を使うと、フィールドは自動で private final、アクセサは public になり、上の判断を自動でしてくれます。

モジュール(module-info の exports)で「public の外側」も絞れる

Java 9 から入ったモジュールシステムを使うと、public なクラスでも「モジュールの外に見せるかどうか」をパッケージ単位で決められます。 module-info.java の exports に書いたパッケージだけが、他のモジュールから使えます。

src
├── shop.core
│   ├── module-info.java
│   └── com/example/shop
│       ├── api/Catalog.java          ← exports する
│       └── internal/PriceTable.java  ← exports しない(でもクラスは public)
└── shop.app
    ├── module-info.java
    └── com/example/app/App.java
// src/shop.core/module-info.java
module shop.core {
    exports com.example.shop.api;      // 公開するのはこのパッケージだけ
}
// src/shop.core/com/example/shop/api/Catalog.java
package com.example.shop.api;

import com.example.shop.internal.PriceTable;

public class Catalog {
    public static int priceOf(String code) { return PriceTable.lookup(code); }
}
// src/shop.core/com/example/shop/internal/PriceTable.java
package com.example.shop.internal;

public class PriceTable {               // public だが exports されていない
    public static int lookup(String code) { return code.length() * 100; }
}
// src/shop.app/module-info.java
module shop.app {
    requires shop.core;
}
// src/shop.app/com/example/app/App.java
package com.example.app;

import com.example.shop.api.Catalog;

public class App {
    public static void main(String[] args) {
        System.out.println("price=" + Catalog.priceOf("A-01"));
    }
}
javac -d out --module-source-path src -m shop.core,shop.app
java -p out -m shop.app/com.example.app.App

実行結果(OpenJDK 21.0.10):

price=400

App.java を書き換えて、exports されていない PriceTable を直接使うと、クラスが public でも次のエラーになります。

import com.example.shop.internal.PriceTable;
// ...
System.out.println("price=" + PriceTable.lookup("A-01"));
src/shop.app/com/example/app/App.java:3: error: package com.example.shop.internal is not visible
import com.example.shop.internal.PriceTable;
                       ^
  (package com.example.shop.internal is declared in module shop.core, which does not export it)
1 error

モジュールを使うと、公開範囲は「モジュール → パッケージ → クラス → メンバー」の順に絞り込まれます。

段階 決めるもの 書く場所
モジュール どのパッケージを外に見せるか module-info.java の exports
パッケージ どのクラスを外に見せるか クラスの public/なし
クラス どのメンバーを外に見せるか メンバーの public/protected/なし/private

module-info.java を置かない通常のクラスパス実行では、exports のチェックは行われません(すべて「名前のないモジュール」として扱われます)。ライブラリを作るときや、大きなアプリを部品ごとに分けたいときに検討するとよい仕組みです。

Java 25 での変更点

アクセス修飾子のルール(§6.6 のアクセス制御)は、Java 21 と Java 25 で変わっていません。 Java 25 の言語仕様(JLS SE 25)の第6章でも、同じ構成で定義されています。そのうえで、関連する変更が2つあります(どちらも公式の JEP で確認したもので、手元の Java 21 では動かしていません)。

変更 内容 アクセス修飾子との関係
JEP 512:Compact Source Files and Instance Main Methods(Java 25 で正式) void main() { ... } だけのファイルを実行できる main メソッドに public static を書かなくてよくなった(private は不可)。従来の public static void main(String[] args) もそのまま使える
JEP 511:Module Import Declarations(Java 25 で正式) import module java.base; でモジュールが exports するパッケージをまとめて import import できるのは exports されているパッケージの public な型だけ。上で見た exports の考え方と同じ

Java 21 で JEP 512 相当を試す場合はプレビュー機能(JEP 445)として --enable-preview が必要で、仕様も Java 25 と細部が異なります。Java 25 の挙動は公式ドキュメントで確認してください。

よくある質問

Q. 修飾子を何も書かないのと public は同じですか? A. 違います。何も書かないと「パッケージプライベート」になり、同じパッケージの中からしか使えません。ただしインターフェースの中のメソッドとフィールドだけは、書かなくても public 扱いです。この違いが「インターフェースを実装したら weaker access privileges のエラーが出た」原因になりやすいので注意してください。

Q. protected は同じパッケージからも見えるのですか? A. はい、見えます。protected は「修飾子なし」の範囲(同じパッケージ)に「別パッケージのサブクラス」を足したものです。同じパッケージに置いたクラスからは、継承していなくても protected メンバーを使えます。「子クラスだけに見せたい」という目的なら、パッケージを分けて設計するとよいです。

Q. private なフィールドはサブクラスに継承されないのですか? A. サブクラスから名前で参照することはできません。ただし、オブジェクトの中には親クラスの private フィールドの値もちゃんと存在していて、親の public/protected メソッドを通して読んだり書いたりできます。「継承されない」は「見えない」という意味だと考えるとわかりやすいです。

Q. リフレクションを使えば private にアクセスできると聞きました。修飾子に意味はありますか? A. setAccessible(true) で private に触れる方法はありますが、名前付きモジュールで opens されていないパッケージに対しては setAccessible(true) が例外になります(JDK 自身の内部 API も Java 17 以降は強くカプセル化されています)。何より、アクセス修飾子はコンパイル時に「使ってよい範囲」を明確にし、変更の影響範囲を小さくするための仕組みです。セキュリティの境界としてではなく、設計を伝える道具として使うのが基本です。

まとめ

  • アクセス修飾子は広い順に public > protected > なし(パッケージプライベート) > private の4段階。protected は「なし」よりも広い。
  • 別パッケージのサブクラスで protected を使えるのは this や自分の型経由だけ。親の型の変数を通すと has protected access になる(clone() が呼べないのも同じ理由)。
  • エラー文は has private access/is not public; cannot be accessed from outside package/has protected access の3つを見分ければ原因がわかる。
  • 既定の修飾子は場所で変わる:クラスはパッケージプライベート、インターフェースのメソッドは public abstract、enum コンストラクタは private、record のアクセサは public。
  • オーバーライドで修飾子は狭められない。インターフェース実装で public を書き忘れると attempting to assign weaker access privileges。
  • 迷ったら private から始め、必要な分だけ広げる。モジュールの exports を使えば public なクラスもモジュールの外から隠せる。
  • Java 25 でもアクセス制御のルールは同じ。JEP 512 で main メソッドの public static が省略可能になった点だけ押さえておく。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Javaアクセス修飾子一覧|privateとprotectedの違いを表とエラー例で」

株式会社RJCでは、Javaのアクセス修飾子とクラス設計を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?