信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
カプセル化・情報隠蔽の思想 ── なぜ private にするのか、getter/setter は何のためにあるか
「とりあえず private にする」から脱却する。情報隠蔽の原則がなぜ生まれたか、public フィールドの何が問題か、getter/setter をただ生やすアンチパターン、イミュータブル設計との関係を解説。
一言結論
カプセル化の本質は「変更の影響を閉じ込めること」。private にする理由は「外から見えないようにする」ではなく「内部の変更が外に波及しないようにする」という変更容易性の確保だ。
Java を学び始めると「フィールドは private にして getter/setter を作る」と教わる。でもなぜかを説明できる人は少ない。「慣習だから」「カプセル化のため」という答えでは本質に届いていない。
カプセル化の本質は**「変更の影響範囲を限定すること」**だ。
1. public フィールドの何が問題か
まず問題を体感する。
class BankAccount {
public int balance; // public フィールド
}
使う側:
BankAccount account = new BankAccount();
account.balance = 1000;
account.balance = -999999; // ← 残高がマイナスになる。防げない
account.balance += 500;
balance が public の場合、誰でも好きな値を書き込める。「残高はマイナスにならない」というルールをコード上で表現できない。
時間が経ってから「残高変更時に取引履歴を記録したい」という要件が出たとしても、フィールドに直接アクセスしている箇所を全部探して修正しないといけない。
// 使っている側が 100 か所あったら、全部直す必要がある
account.balance += 500; // ←これが100か所に散らばっている
2. private + メソッドにする意味
class BankAccount {
private int balance; // 外から直接触れない
public void deposit(int amount) {
if (amount <= 0) throw new IllegalArgumentException("入金額は正の数");
balance += amount;
recordTransaction(amount); // ← 将来追加しやすい
}
public void withdraw(int amount) {
if (amount <= 0) throw new IllegalArgumentException("出金額は正の数");
if (balance < amount) throw new IllegalStateException("残高不足");
balance -= amount;
recordTransaction(-amount);
}
public int getBalance() {
return balance;
}
}
変更の経路が deposit・withdraw の 2 つに絞られた。ルールの検証・履歴の記録・将来の拡張はすべてこのメソッドの中に閉じ込められる。
【情報隠蔽の効果】
変更前:
外の世界 ──── balance ──── BankAccount 内部
(100 箇所から直接アクセスされる)
変更後:
外の世界 ── deposit/withdraw ── balance
(アクセスの窓口が 2 つだけ)
**「private にする」= 「変更の窓口をコントロールする」**ということだ。
3. getter/setter を「ただ生やす」アンチパターン
よくあるアンチパターン:
class Person {
private String name;
private int age;
// IDE が自動生成した getter/setter をそのまま全部生やす
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; }
}
そして使う側:
person.setAge(-5); // 年齢がマイナス。setterにバリデーションなし
person.setName(null); // null が入る
これは public フィールドとほぼ同じだ。setter が何の制御もせず値を受け入れるなら、情報隠蔽の恩恵がない。
setter は本当に必要か考える
// ✅ 変更できない不変クラス(イミュータブル)
class Person {
private final String name;
private final int age;
public Person(String name, int age) {
if (age < 0) throw new IllegalArgumentException("年齢は 0 以上");
if (name == null || name.isBlank()) throw new IllegalArgumentException("名前は必須");
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
// setter なし → 一度作ったら変えられない
}
変更が必要な場合は新しいインスタンスを返す:
class Person {
// ...
public Person withAge(int newAge) {
return new Person(this.name, newAge); // 新しいインスタンスを返す
}
}
4. 情報隠蔽が「変更容易性」を生む理由
1968 年、David Parnas は「モジュール化の基準として、情報隠蔽を使うべき」という論文を発表した。これが現代のオブジェクト指向設計の根拠になっている。
主張の核心:「変わりそうな部分を隠す」。
class UserStorage {
// ← 内部が HashMap だということを外に見せない
private Map<Integer, User> storage = new HashMap<>();
public void save(User user) {
storage.put(user.getId(), user);
}
public Optional<User> findById(int id) {
return Optional.ofNullable(storage.get(id));
}
}
将来 HashMap から Database に変えたくなっても、save と findById のシグネチャさえ変えなければ、使っている側のコードは一切変更しなくていい。
// 将来の変更: 内部の実装を変えるだけ
class UserStorage {
private UserRepository db; // DB に変えた
public void save(User user) {
db.insert(user); // 中身を変えたが
}
public Optional<User> findById(int id) {
return db.findById(id); // シグネチャは同じ
}
// 使う側のコードは変えなくていい!
}
5. アクセス修飾子の使い分けの指針
| 修飾子 | 使う場面 |
|---|---|
private | デフォルト。外から触る理由がないなら private |
protected | サブクラスに拡張ポイントを提供したいとき(継承設計) |
(package-private) | 同パッケージ内のクラス同士が協調する内部実装 |
public | 意図的に外部に公開する API のみ |
よくある誤解:「protected は private より少し緩い」。実際は「サブクラスに拡張ポイントを提供する」という設計の意図がある。みだりに protected にすると、継承の文脈で意図せず変更の経路が増える。
実務の経験則:「最初は private、必要になったら広げる」。逆(最初 public、後で絞る)は後方互換性の問題で難しい。
6. イミュータブルクラスの 4 条件
Java で String が private final な理由はイミュータブル(不変)だからだ。イミュータブルクラスにする条件:
public final class Money { // 1. クラスを final にする(継承禁止)
private final int amount; // 2. フィールドを final にする
private final String currency;
public Money(int amount, String currency) { // 3. コンストラクタでのみ値をセット
if (amount < 0) throw new IllegalArgumentException();
this.amount = amount;
this.currency = currency;
}
public int getAmount() { return amount; } // 4. getter は値のコピーを返す
public String getCurrency() { return currency; }
// setter なし
// 変更が必要な場合は新しいインスタンスを返す
public Money add(Money other) {
if (!this.currency.equals(other.currency)) throw new IllegalArgumentException();
return new Money(this.amount + other.amount, this.currency);
}
}
イミュータブルの利点:
- スレッドセーフ: 変更されないのでロックが不要
- バグが少ない: 共有しても誰かが変えることがない
- キャッシュ可能: 同じ値なら同じインスタンスを使い回せる(String pool の設計理由)
まとめ
| 概念 | 正体 |
|---|---|
private にする理由 | 変更の窓口をメソッドに絞り、影響範囲を限定する |
| getter/setter の意味 | ただの公開手段ではなく、バリデーション・ロギング・変換の場所 |
| セッターを生やすな | 不変にできるなら不変にする。変更可能性は明示的に設計する |
| イミュータブル | 変更しないことでスレッドセーフ・キャッシュ可能・バグ減少 |
| 情報隠蔽の本質 | 「見えなくする」より「変更の影響を閉じ込める」設計思想 |