2.データクラス
学習目標
- データクラスの役割と典型例を説明できる
- カプセル化の動機を説明し、
privateフィールドと getter / setter を書ける - 変更不要なフィールドを
finalで保護し、不変と可変を意図的に選択できる
name と age という 2 つのフィールドを持つ Person のように、データを保持することを主な目的とするクラスを データクラス と呼びます。
データクラスの例には次のようなものがあります。
Person: 名前と年齢を持つ人Book: タイトル・価格・在庫を持つ書籍Point: x 座標と y 座標を持つ点
1. カプセル化
Section titled “1. カプセル化”Person のフィールド name と age は、外部から制限なくアクセスできます。この状態では、不正な値の代入を防ぐ手段がありません。
public class Person { String name; int age;
public Person(String name, int age) { this.name = name; this.age = age; }}Person p = new Person("田中", 25);p.age = -5; // 不正な値でも代入が通るp.name = null; // null でも代入が通るSystem.out.println(p.age); // -5age に -5 を代入することも、name に null を代入することも、Person 自身では防げません。呼び出し元のコードに依存するため、誤った値が渡されると整合性の崩れたデータが残ります。これを防ぐには、フィールドへのアクセスを制御する仕組みが必要です。
1-1. アクセス修飾子
Section titled “1-1. アクセス修飾子”フィールドやメソッドには、外からのアクセスを制御する アクセス修飾子 を指定できます。
| アクセス修飾子 | クラスの外から | 同じクラスの中から |
|---|---|---|
public | アクセスできる | アクセスできる |
private | アクセスできない | アクセスできる |
Person のフィールドを private にした場合、クラスの外から p.name のように直接アクセスしようとするとコンパイルエラーになります。
一方、同じクラスの中のメソッドは private フィールドにアクセスできます。クラスの外からも読み書きするには、書き換え用のメソッドと読み取り用のメソッドを public で用意します。
1-2. setter
Section titled “1-2. setter”クラスの外から private フィールドを書き換えるには、書き換え用のメソッドを public で用意します。
public class Person { private String name; private int age;
public Person(String name, int age) { this.name = name; this.age = age; }
public void setAge(int age) { this.age = age; }}Person p = new Person("田中", 25);p.setAge(26); // age を 26 に更新このような書き換え用のメソッドを setter と呼びます。メソッド名は set の後にフィールド名を続けるのが Java の慣習です。フィールドの書き換えは setter を経由する形に統一されます。
setter 経由にする利点は、書き換えの前後に処理を追加できる点にあります。たとえば、setAge で負の年齢を弾けます。
public void setAge(int age) { if (age < 0) { System.out.println("不正な年齢: " + age); return; } this.age = age;}Person p = new Person("田中", 25);p.setAge(-5);不正な年齢: -5-5 を渡すと、println でメッセージを出力し、return で処理を中断します。フィールドへの代入は実行されず、age は元の 25 のままです。setter を経由することで、不正な値を検出して処理を中断できます。
name のように変更させたくないフィールドでは、setter を作らないという選択肢もあります。何を公開して何を隠すかを設計時に決定します。
1-3. getter
Section titled “1-3. getter”private フィールドは、外部からの読み取りもコンパイルエラーになります。読み取り用に public メソッドを定義します。
public String getName() { return name;}
public int getAge() { return age;}Person p = new Person("田中", 25);System.out.println(p.getName()); // 田中System.out.println(p.getAge()); // 25このような読み取り用のメソッドを getter と呼びます。メソッド名は get の後にフィールド名を続けるのが Java の慣習です。
private でフィールドを保護し、getter / setter を経由した操作だけを許す設計を カプセル化 と呼びます。private で直接代入を禁止し、setter で検査することで、フィールドの整合性を保てます。
2. 不変性
Section titled “2. 不変性”データクラスのフィールドには、作成後に変更を許可するものと、作成後の変更を禁止するものがあります。
Person の場合、age は誕生日ごとに変わるので可変、name は通常変わらないので不変、という設計を選びます。Point の場合、座標自体は変えず、移動した結果として新しい Point を作る不変設計を選びます。
2-1. final フィールド
Section titled “2-1. final フィールド”変更させたくないフィールドには final を付けます。final フィールドは、コンストラクターで一度初期化された後、再代入できません。
class クラス名 { private final データ型 フィールド名; ...}public class Person { private final String name; private int age;
public Person(String name, int age) { this.name = name; this.age = age; }}name を private final にすると、Person のインスタンスを生成した後は、クラスの内外を問わず name への再代入がコンパイルエラーになります。setter を作らない選択だけでは「クラス内からは書き換え可能」な状態が残りますが、final を付けるとコンパイラーが書き換えを禁止します。
2-2. 不変クラスの設計
Section titled “2-2. 不変クラスの設計”Person のように一部のフィールドだけ不変にする設計のほかに、すべてのフィールドを不変にする設計もあります。すべてのフィールドが final で、変更メソッドを持たないクラスを 不変クラス(イミュータブルクラス)と呼びます。String、Integer、LocalDate などの標準ライブラリのクラスは不変クラスです。
不変クラスの利点は次のとおりです。
- デバッグしやすい: 値がいつどこで変わったかを追う必要がない
- コピー不要: インスタンスの参照を直接渡せる
不変クラスで値を変更したい場合は、変更後の値を持つ新しいインスタンスを生成して返します。たとえば座標 (3, 5) の Point を (4, 5) に動かす操作は、元の Point を書き換えず、新しい Point(4, 5) を返します。
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; }
public Point move(int dx, int dy) { return new Point(x + dx, y + dy); }}Point p = new Point(3, 5);Point moved = p.move(1, 0);
System.out.println(p.getX() + ", " + p.getY()); // 3, 5System.out.println(moved.getX() + ", " + moved.getY()); // 4, 5p.move(1, 0) は p を変更せず、新しい Point を返します。元の p は座標 (3, 5) のままで、移動結果は別のインスタンス moved に代入されます。
2-3. 不変と可変の選び方
Section titled “2-3. 不変と可変の選び方”すべてのフィールドを不変にできれば設計はシンプルになりますが、業務ロジック上、変更が前提となるフィールドもあります。判断の基準は次のとおりです。
| フィールドの性質 | 設計 |
|---|---|
| 作成後に変わらない値(識別子、座標、金額など) | final + setter なし |
| 業務上変更される値(在庫、ステータス、年齢など) | final を付けず setter で検査 |
Person の場合、name は不変(final、setter なし)、age は可変(setter で検査)、という組み合わせの一例です。
3. 完成形のコード
Section titled “3. 完成形のコード”カプセル化と不変・可変の選択を施した Person のコード全体は次のとおりです。
public class Person { private final String name; private int age;
public Person(String name, int age) { this.name = name; this.age = age; }
public String getName() { return name; }
public int getAge() { return age; }
public void setAge(int age) { if (age < 0) { System.out.println("不正な年齢: " + age); return; } this.age = age; }}name は private final で setter なしの不変フィールド、age は private で setter による検査つきの可変フィールドです。Person のインスタンスを生成した後、name の書き換えはコンパイラーが禁止し、age の書き換えは setter 経由に統一されます。