信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
アクセス修飾子はなぜあるのか — カプセル化という「壁を作る」設計思想
public/private/protected/package-privateの意味と使い分け。なぜprivateにするのか、getterとsetterは何のためにあるのかを根本の思想から解説する。
一言結論
アクセス修飾子はカプセル化のための道具。privateで「内部実装」と「外部インターフェース」を分離することで、内部の変更が外部に波及しない設計を実現する。
なぜ「見える範囲」を制限するのか
Javaには4種類のアクセス修飾子がある:
public class BankAccount {
public String owner; // どこからでも見える
protected double balance; // 同パッケージ + 子クラスから見える
double limit; // 同パッケージからのみ見える(package-private)
private String pin; // このクラスの中からのみ見える
}
「全部publicにすれば便利なのでは?」と思うかもしれない。実際、初心者のコードは全部publicになりがちだ。でも全部公開すると何が起きるのか。
全部publicにしたとき何が起きるか
public class BankAccount {
public double balance; // 残高を公開してしまった
}
// 利用者側(悪意なくても起きる)
BankAccount account = new BankAccount();
account.balance = -99999; // 残高をマイナスにできてしまう
残高は通常マイナスにできないはずだ。でもbalanceがpublicなら、誰でも直接書き換えられる。銀行口座の「内部ルール」を無視した操作が可能になってしまう。
さらに困るのは、内部実装を変えられなくなることだ:
// 最初の実装
public class Temperature {
public double celsius; // 摂氏で管理していた
}
// 利用者が直接フィールドを使う
Temperature t = new Temperature();
t.celsius = 100.0;
後になって「内部はケルビンで管理したい」と変えようとしても:
public class Temperature {
public double kelvin; // 変更!
// celsius は削除した
}
t.celsiusを使っているコードが全部壊れる。内部実装を変えると外部が壊れる、という事態になる。
カプセル化:「見せるもの」と「隠すもの」を分ける
カプセル化とは「内部の実装を隠し、外部から安全にアクセスできる窓口だけを公開する」設計思想だ。
public class BankAccount {
private double balance; // 内部実装(隠す)
// 公開する窓口
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("金額は正の数で");
balance += amount;
}
public void withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException("金額は正の数で");
if (amount > balance) throw new IllegalStateException("残高不足");
balance -= amount;
}
public double getBalance() {
return balance;
}
}
これで:
BankAccount account = new BankAccount();
account.balance = -99999; // ❌ エラー(privateなので直接アクセス不可)
account.deposit(-100); // ❌ 例外がスローされる(検証が入っている)
account.deposit(1000); // ✅ 正常
account.withdraw(500); // ✅ 正常
account.withdraw(10000); // ❌ 例外(残高不足)
「不正な状態のオブジェクト」を作れなくなった。これがカプセル化の恩恵だ。
getterとsetterはなぜあるのか
privateなフィールドに読み書きするためのgetXxx()とsetXxx()がよく書かれる:
public class Person {
private String name;
private int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
「それなら最初からpublicにすれば同じでは?」という疑問は正しい感覚だ。単純なgetterとsetterなら確かにpublicフィールドと変わらない。
getterとsetterの本当の価値は後から中に処理を追加できる点にある:
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年齢が不正です: " + age);
}
this.age = age;
}
public String getName() {
return name != null ? name : "名無し"; // nullの場合のデフォルト値
}
最初は素通しで書いておいて、後からバリデーションやロジックを追加できる。publicフィールドにしてしまうと、後から追加できない。
また、読み取り専用にもできる:
public double getBalance() { return balance; }
// setBalance()は公開しない → 外から書き換えられない
4種類のアクセス修飾子の具体的な範囲
同クラス 同パッケージ 子クラス どこでも
private | ✅ | | |
(修飾子なし) | ✅ | ✅ | |
protected | ✅ | ✅ | ✅ |
public | ✅ | ✅ | ✅ | ✅
package-private(修飾子なし) は「同じパッケージ内のクラスからは見える」。パッケージは関連するクラスをまとめる単位なので、「パッケージ内で共有するが外には見せない」という細かい制御ができる。
protected は「子クラスには見せるが外部には隠す」。継承して使うことを前提としたクラスで使う。
実際の使い分け
フィールドは基本的に private
→ 直接アクセスされると内部ルールを破られる可能性がある
メソッドは目的で判断
→ 外部から使ってほしいメソッド: public
→ 内部の補助処理(外部から呼ばれる必要がない): private
→ 子クラスにカスタマイズさせたい: protected
クラスは基本的に public
→ ただし「このパッケージ内でだけ使う補助クラス」はpackage-private
まとめ
アクセス修飾子の目的 = カプセル化(内部実装と外部インターフェースの分離)
privateを使う理由 = 「不正な状態のオブジェクト」を作らせない + 内部実装を変更できるようにする
getterとsetterの価値 = 後からバリデーションやロジックを追加できる柔軟性
全部publicにする問題 = 内部実装を変えると外部が壊れる・不正な操作ができてしまう
private = このクラスだけ
(なし) = 同パッケージだけ
protected = 同パッケージ + 子クラス
public = どこからでも
「privateにしておけとりあえず安全」は正しい。でも本質は「何を外部に公開するかを意識的に選ぶ」ことだ。壁を作ることで、内部をどう変えても外部に影響が及ばない堅牢な設計が生まれる。