1. カプセル化の定義
カプセル化は、オブジェクト指向プログラミング(OOP)の基本原則の一つです。平たく言えば、カプセル化とはオブジェクトの内部(「中身」)を隠し、用意された「扉」だけ(公開メソッド)を通じてアクセスさせることです。
最新型のコーヒーマシンを思い浮かべてください。利用者が見るのはボタンとディスプレイだけで、ボイラーやポンプ、内部のチューブの仕組みを知る必要はありません。利用者は「カプチーノ」を押すだけで結果が得られます。内部はすべて隠されています。これがカプセル化です。
Java(および他の OOP 言語)では、カプセル化は次の手段で実現します。
- データ隠蔽 — クラスのフィールドは private(少なくとも public にはしない)として宣言する。
- 公開インターフェース — 外部に公開するのは、オブジェクトの利用者に本当に必要なメソッドだけにする。
図: カプセル化のイメージ
+-------------------------------+
| Class Student |
|-------------------------------|
| - name: String | // private field
| - age: int | // private field
|-------------------------------|
| + getName(): String | // public method
| + setName(String): void | // public method
| + getAge(): int | // public method
| + setAge(int): void | // public method
+-------------------------------+
ここで記号 - は private(非公開)を、+ は public(外部から利用可能)を意味します。
getter と setter とは?
カプセル化の必要性に入る前に、getter と setter を手短に紹介します。これはクラスの private フィールドと「やり取り」するための特別なメソッドです。
ゲッター(getter)は、private フィールドの値を取得するメソッドです。通常は getFieldName() のように命名します。
セッター(setter)は、private フィールドの値を設定するメソッドです。通常は setFieldName(value) のように命名します。
簡単な例:
public class Student {
private String name; // private フィールド - 外部からは見えない
// ゲッター: 学生の名前を返す
public String getName() {
return name;
}
// セッター: 学生の名前を設定する
public void setName(String name) {
this.name = name;
}
}
動作例:
Student student = new Student();
student.setName("Vasya"); // セッターで名前を設定
String name = student.getName(); // ゲッターで名前を取得
ゲッターとセッターは、オブジェクトへの「丁寧なお願い」のようなものだと考えてください。直接ポケットに手を突っ込む(student.name = "Vasya")代わりに、「名前を設定してください」(student.setName("Vasya"))と丁寧に依頼します。
先取り情報: 数回後の講義でゲッターとセッターを詳しく学び、コツや活用法も紹介します。今は基本的な考え方だけ理解しておけば十分です。
2. なぜカプセル化が必要か?
不適切な利用からデータを保護するため
もしクラスのすべてのフィールドが public だったら、外部のどんなコードでも値を好き勝手に変えられてしまいます。
Student s = new Student();
s.age = -1000; // おっと、吸血鬼の学生!
これは危険です。プログラムは予測不能な振る舞いを始め、バグは思わぬ場所で発生するでしょう。
外部コードに影響を与えずに内部実装を変更できる
カプセル化により、クラス内部の仕組みを変えても、それを利用するコードを壊さずに済みます。たとえば、データの保持方法を変えたり、メソッドにバリデーションを追加しても、クラスの利用者は同じメソッドを呼び続けるだけで変更に気づきません。
コードの可読性と保守性の向上
内部の詳細を隠すことで、外部インターフェースはよりシンプルで分かりやすくなります。クラスを使う側のプログラマは内部の仕組みを理解する必要はなく、利用可能なメソッドとその働きだけを知っていれば十分です。
身近な例
スマートフォンの使い方を思い出してください。画面のタップがどのように処理されるか、バッテリーの構造や通信モジュールの動作がどうなっているかを考えなくても、アイコンやボタンという分かりやすいインターフェースから必要な機能を呼び出します。メーカーが内部実装を変えても、あなたは気づきません。
コードでの実例
クラス BankAccount があるとします。古いバージョンでは、残高は「1.000.50」のようなドット区切りの文字列で保持していました。後で開発者が残高を数値の double で持つことにしました。もしフィールドが public だったら、account.balance に直接アクセスしていた古いコードはすべて壊れてしまいます。
しかし、フィールドを隠して deposit() と getBalance() だけを公開していれば、外部コードは変更に気づきません。
public class BankAccount {
private double balance; // フィールドは非公開
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public double getBalance() {
return balance;
}
}
明日もし残高をセント単位(long)で保持したくなっても、クラス内部の実装を変えるだけで、deposit() と getBalance() を呼ぶ既存コードはこれまで通り動作します。
3. 悪いカプセル化と良いカプセル化の例
悪い例: public フィールド
public class Student {
public String name;
public int age;
}
このアプローチの問題点:
- どんなコードでも、フィールドに不正な値を含め任意の値を代入できてしまう。
- データ検証を追加できない。
- フィールドの型や構造を変更すると、それを利用しているコードをすべて修正しなければならない。
良い例: private フィールドと public メソッド
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
// 検証を追加できる
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("名前は空にできません");
}
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
// 年齢が負でないことを確認
if (age < 0) {
throw new IllegalArgumentException("年齢は負の数にできません");
}
this.age = age;
}
}
利点:
- 外部コードはフィールドを直接変更できず、メソッド経由に限定できる。
- 検証、ログ、付随処理(統計更新など)を追加できる。
- 内部表現が変わっても(例: 年齢の表現形式を変更)、外部インターフェースはそのまま保てる。
使用例
Student s = new Student();
s.setName("Alisa");
s.setAge(20);
System.out.println(s.getName() + "、年齢: " + s.getAge());
負の年齢を代入しようとすると、実行時にエラーになります。プログラムは不正な操作から守られます。
4. 他の OOP 原則との関係
カプセル化は OOP の「母」のような存在です。これがなければ、継承もポリモーフィズムも抽象化も成立しません。これらは後で詳しく学びますが、ここでは簡単に触れておきます。
- 継承(extends)は、既存のクラスを基に新しいクラスを作り、その振る舞いを拡張・変更できます。もし内部が丸見えなら、サブクラスが重要な部分をうっかり壊してしまうかもしれません。
- ポリモーフィズム(異なるクラスのオブジェクトが同じメッセージに対して異なる反応をする性質)は、内部実装と外部インターフェースが明確に分離されていなければ成り立ちません。
- 抽象化は、本質的な特徴だけを取り出し詳細を隠すことです。カプセル化は抽象化を実践する助けになります。
たとえ話
自動車を考えてみましょう。運転者が触れるのはハンドル、ペダル、レバーといったインターフェースだけです。その他(エンジン、変速機、電子制御)はボンネットの下に隠されています。もし運転者がエンジンの一つひとつのネジを直接操作できたら、事故はずっと増えるでしょう。
5. 実践例: 私たちのアプリでのカプセル化
学習用アプリを発展させていきましょう。例として「アドレス帳」を考えます。Contact クラスが名前と電話番号を保持します。
カプセル化なし(アンチパターン):
public class Contact {
public String name;
public String phone;
}
使用例:
Contact contact = new Contact();
contact.name = ""; // あっ! 名前が空
contact.phone = null; // 電話番号が未設定
カプセル化あり(正しいアプローチ):
public class Contact {
private String name;
private String phone;
public String getName() {
return name;
}
public void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("連絡先の名前は空にできません");
}
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
if (phone == null || phone.isBlank()) {
throw new IllegalArgumentException("電話番号は空にできません");
}
this.phone = phone;
}
}
これで外部コードは空の名前や電話番号を設定できません。
Contact contact = new Contact();
contact.setName("Ivan");
contact.setPhone("+1-999-123-45-67");
空の名前を設定しようとすれば、プログラムはエラーを投げます。
6. 役立つニュアンス
カプセル化と長期的な保守
小さな学習用プロジェクトでは「常識に任せればよい」と思いがちです。誰も負の年齢や空の名前を設定しないだろう、と。しかしプロジェクトが大きくなり、他の開発者が参加し、あなた自身も数カ月後には実装の細部を忘れてしまいます。そこでカプセル化が混乱から救ってくれます。
- クラス内部を容易に変更できる — 例えば電話番号を文字列ではなく PhoneNumber 型のオブジェクトとして保持したくなったら、実装だけを変えればよく、外部コードは触らずに済みます。
- テストが容易 — 変更がメソッド経由に限定されれば、どのデータがいつ変わるかを追跡しやすくなります。
- バグが減る — 不正な値や偶発的な変更から守れます。
質問: getter と setter は常に必要ですか?
初心者はよく「カプセル化=private フィールドと public の getter/setter だから、すべてのフィールドに getter と setter を作るべきだ」と考えます。必ずしもそうではありません。
- 読み取り専用にすべきフィールド(例: 一意な識別子)もあります。その場合は getter だけにします。
- 外部に公開する必要のないフィールドもあります。その場合は getter も setter も作りません。
- 値を変更できるのがクラス内部だけでよい場合、setter を private にできます。
黄金律: 外部コードに本当に必要なデータとメソッドだけを公開しましょう。
可視化: アプローチの比較
| アプローチ | フィールドアクセスの例 | 制御の可否 | 安全性 |
|---|---|---|---|
| public フィールド | |
いいえ | 低い |
| private フィールド + メソッド | |
はい | 高い |
7. カプセル化でよくある誤り
エラー1: クラスのすべてのフィールドを public にしてしまう。 初心者に最も多いミスです。こうしたコードはすぐに手に負えなくなります。誰もがあなたの知らないところでデータを変更できるからです。時間を節約したいときでも、これは避けましょう。
エラー2: 検証やロジックのない getter/setter。 アクセスメソッドを作るなら、バリデーションを入れましょう。不正な値の代入を許してはいけません。単にパラメータをフィールドに「コピー」するだけが最善とは限りません。
エラー3: 内部構造の早すぎる公開。 予防的に全フィールドの getter/setter を作ってしまうと、公開範囲が過剰になり、後で変更が難しくなるリスクがあります。
エラー4: 可変オブジェクトをそのまま返す。 フィールドが可変オブジェクト(例: リスト)の場合、getter で直接返さないようにします。コピーを返すか、不変にするほうが安全です。
GO TO FULL VERSION