こんにちは、かつコーチです。
前回はコンストラクタで初期値を安全に設定する方法を解説しました。
今回は、フィールドを外部から自由に触らせないようにする「カプセル化」と、そのためのgetter・setterの書き方を解説します。
カプセル化とは?
フィールドを外部から隠す設計
カプセル化とは、クラスの内部データ(フィールド)を外部から直接操作できないように隠し、決められた方法(メソッド)を通してのみアクセスさせる設計のことです。
オブジェクト指向における重要な考え方の1つで、「情報隠蔽」とも呼ばれます。
これまでの記事のコードは、実はフィールドを外部から自由に書き換えられる状態でした。
public class Dog {
String name;
int age;
}
Dog dog = new Dog();
dog.age = -5; // 年齢がマイナスになってしまう!でもコンパイルエラーにはならない
犬の年齢がマイナスになるのは明らかにおかしいですが、Javaはこれをエラーとして検出してくれません。
このような「本来あり得ない値」が入り込むのを防ぐのがカプセル化の目的です。
なぜそれが必要なのか
フィールドを外部に公開したままだと、以下のような問題が起こります。
- どこからでも自由に値を書き換えられるため、不正な値が入り込む可能性がある
- 後からフィールドの型や名前を変更すると、そのフィールドを直接使っているすべてのコードを修正する必要がある
- 値を変更した際に「ついでに実行したい処理(ログ出力やバリデーションなど)」を差し込む場所がない
カプセル化しておけば、フィールドへのアクセス経路を一本化できるため、後からルールを追加・変更しやすくなります。
getter・setterの基本
アクセス修飾子privateでフィールドを隠す
まず、フィールドにprivateというアクセス修飾子(クラスやメンバへのアクセス範囲を制限するキーワード)を付けて、クラスの外から直接見えないようにします。
public class Dog {
private String name;
private int age;
}
この状態でdog.age = -5;のように外部から書こうとすると、以下のようなコンパイルエラーになります。
error: age has private access in Dog
getterで値を取得する
外部から値を「読み取る」ための専用メソッドをgetterと呼びます。
慣習としてgetフィールド名という名前を付けます。
public class Dog {
private String name;
private int age;
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
setterで値を設定する(バリデーション付き)
外部から値を「設定する」ための専用メソッドをsetterと呼びます。
慣習としてsetフィールド名という名前を付け、引数として新しい値を受け取ります。
setterの最大のメリットは、値を設定する際にチェック処理を挟めることです。
public class Dog {
private String name;
private int age;
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("年齢はマイナスにできません");
}
this.age = age;
}
public int getAge() {
return age;
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.setAge(3);
System.out.println(dog.getAge()); // 3
dog.setAge(-5); // 実行時に例外が発生する
}
}
このように、setterの中でバリデーションを行うことで「マイナスの年齢」というあり得ない状態をブロックできます。
フィールドを直接公開していたら、こうしたチェックを差し込む場所自体がありませんでした。
よくあるつまずきポイント・エラー対処
getter・setterを作らずフィールドをpublicのままにしてしまう
初心者のうちは「private にするとエラーが出るから」という理由で、フィールドをpublicのまま放置してしまいがちです。
❌Before
public class Account {
public int balance; // 誰でも自由に書き換えられる
public void withdraw(int amount) {
balance -= amount;
}
}
Account account = new Account();
account.balance = 1000;
account.balance = -100000; // 残高がマイナスになっても防げない
✅After
public class Account {
private int balance;
public void deposit(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("入金額は正の数にしてください");
}
balance += amount;
}
public void withdraw(int amount) {
if (amount > balance) {
throw new IllegalArgumentException("残高が不足しています");
}
balance -= amount;
}
public int getBalance() {
return balance;
}
}
残高(balance)を直接いじらせず、deposit・withdrawという「意味のある操作」を通してのみ変更できるようにすることで、不正な状態を防いでいます。
すべてのフィールドに機械的にgetter・setterを付けてしまう
私が実務でコードレビューを受けた際に指摘されたのが、「すべてのフィールドに何も考えずgetter・setterを自動生成で付けてしまう」というアンチパターンです。
IDE(IntelliJ IDEA)には全フィールド分のgetter・setterをワンクリックで自動生成する機能がありますが、これに頼り切ると以下のようなコードになりがちです。
// すべてがsetter経由で自由に書き換え可能になっており、
// カプセル化の意味がほとんどなくなっている
public class Order {
private int quantity;
private double price;
public void setQuantity(int quantity) { this.quantity = quantity; }
public int getQuantity() { return quantity; }
public void setPrice(double price) { this.price = price; }
public double getPrice() { return price; }
}
これではprivateにした意味が薄れ、実質的にpublicフィールドと変わりません。
本当に外部から変更してよいフィールドにだけsetterを用意し、変更されては困るフィールドにはgetterのみを用意する、という判断を意識的に行うことが大切です。
応用・一歩先の使い方
イミュータブル(変更不可)なクラスにする
setterを一切用意せず、コンストラクタでのみ値を設定できるようにすると、生成後に状態が変わらない「イミュータブル(不変)」なクラスを作れます。
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
}
finalを付けることで、コンストラクタ以外での再代入をコンパイル時に禁止できます。
イミュータブルなクラスは、複数の処理から同時にアクセスされても値が勝手に変わらないため、バグを防ぎやすいというメリットがあります。
まとめ
この記事のポイント
- カプセル化とはフィールドを
privateで隠し、getter・setterを通してのみアクセスさせる設計のこと - getterは値を読み取るメソッド、setterは値を設定するメソッドで、慣習的に
get・set+フィールド名という命名にする - setterにバリデーションを入れることで、不正な値の代入を防げる
- すべてのフィールドに機械的にgetter・setterを付けるとカプセル化の意味が薄れるため、本当に必要なアクセスだけを公開する
次に読むべき記事
カプセル化の基本を理解したら、次はクラス同士の親子関係を作る「継承の基本とsuper」に進みましょう。
タグ: Java, 初心者向け, オブジェクト指向