Skip to content
Playground

2.データクラス

学習目標

  • データクラスの役割と典型例を説明できる
  • カプセル化の動機を説明し、private フィールドと getter / setter を書ける
  • 変更不要なフィールドを final で保護し、不変と可変を意図的に選択できる

nameage という 2 つのフィールドを持つ Person のように、データを保持することを主な目的とするクラスを データクラス と呼びます。

データクラスの例には次のようなものがあります。

  • Person: 名前と年齢を持つ人
  • Book: タイトル・価格・在庫を持つ書籍
  • Point: x 座標と y 座標を持つ点

Person のフィールド nameage は、外部から制限なくアクセスできます。この状態では、不正な値の代入を防ぐ手段がありません。

Person.java
public class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
Main.java
Person p = new Person("田中", 25);
p.age = -5; // 不正な値でも代入が通る
p.name = null; // null でも代入が通る
System.out.println(p.age); // -5

age-5 を代入することも、namenull を代入することも、Person 自身では防げません。呼び出し元のコードに依存するため、誤った値が渡されると整合性の崩れたデータが残ります。これを防ぐには、フィールドへのアクセスを制御する仕組みが必要です。

フィールドやメソッドには、外からのアクセスを制御する アクセス修飾子 を指定できます。

アクセス修飾子クラスの外から同じクラスの中から
publicアクセスできるアクセスできる
privateアクセスできないアクセスできる

Person のフィールドを private にした場合、クラスの外から p.name のように直接アクセスしようとするとコンパイルエラーになります。

一方、同じクラスの中のメソッドは private フィールドにアクセスできます。クラスの外からも読み書きするには、書き換え用のメソッドと読み取り用のメソッドを public で用意します。

クラスの外から private フィールドを書き換えるには、書き換え用のメソッドを public で用意します。

Person.java
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;
}
}
Main.java
Person p = new Person("田中", 25);
p.setAge(26); // age を 26 に更新

このような書き換え用のメソッドを setter と呼びます。メソッド名は set の後にフィールド名を続けるのが Java の慣習です。フィールドの書き換えは setter を経由する形に統一されます。

setter 経由にする利点は、書き換えの前後に処理を追加できる点にあります。たとえば、setAge で負の年齢を弾けます。

Person.java
public void setAge(int age) {
if (age < 0) {
System.out.println("不正な年齢: " + age);
return;
}
this.age = age;
}
Main.java
Person p = new Person("田中", 25);
p.setAge(-5);
不正な年齢: -5

-5 を渡すと、println でメッセージを出力し、return で処理を中断します。フィールドへの代入は実行されず、age は元の 25 のままです。setter を経由することで、不正な値を検出して処理を中断できます。

name のように変更させたくないフィールドでは、setter を作らないという選択肢もあります。何を公開して何を隠すかを設計時に決定します。

private フィールドは、外部からの読み取りもコンパイルエラーになります。読み取り用に public メソッドを定義します。

Person.java
public String getName() {
return name;
}
public int getAge() {
return age;
}
Main.java
Person p = new Person("田中", 25);
System.out.println(p.getName()); // 田中
System.out.println(p.getAge()); // 25

このような読み取り用のメソッドを getter と呼びます。メソッド名は get の後にフィールド名を続けるのが Java の慣習です。

private でフィールドを保護し、getter / setter を経由した操作だけを許す設計を カプセル化 と呼びます。private で直接代入を禁止し、setter で検査することで、フィールドの整合性を保てます。

データクラスのフィールドには、作成後に変更を許可するものと、作成後の変更を禁止するものがあります。

Person の場合、age は誕生日ごとに変わるので可変、name は通常変わらないので不変、という設計を選びます。Point の場合、座標自体は変えず、移動した結果として新しい Point を作る不変設計を選びます。

変更させたくないフィールドには final を付けます。final フィールドは、コンストラクターで一度初期化された後、再代入できません。

class クラス名 {
private final データ型 フィールド名;
...
}
Person.java
public class Person {
private final String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}

nameprivate final にすると、Person のインスタンスを生成した後は、クラスの内外を問わず name への再代入がコンパイルエラーになります。setter を作らない選択だけでは「クラス内からは書き換え可能」な状態が残りますが、final を付けるとコンパイラーが書き換えを禁止します。

Person のように一部のフィールドだけ不変にする設計のほかに、すべてのフィールドを不変にする設計もあります。すべてのフィールドが final で、変更メソッドを持たないクラスを 不変クラス(イミュータブルクラス)と呼びます。StringIntegerLocalDate などの標準ライブラリのクラスは不変クラスです。

不変クラスの利点は次のとおりです。

  • デバッグしやすい: 値がいつどこで変わったかを追う必要がない
  • コピー不要: インスタンスの参照を直接渡せる

不変クラスで値を変更したい場合は、変更後の値を持つ新しいインスタンスを生成して返します。たとえば座標 (3, 5)Point(4, 5) に動かす操作は、元の Point を書き換えず、新しい Point(4, 5) を返します。

Point.java
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);
}
}
Main.java
Point p = new Point(3, 5);
Point moved = p.move(1, 0);
System.out.println(p.getX() + ", " + p.getY()); // 3, 5
System.out.println(moved.getX() + ", " + moved.getY()); // 4, 5

p.move(1, 0)p を変更せず、新しい Point を返します。元の p は座標 (3, 5) のままで、移動結果は別のインスタンス moved に代入されます。

すべてのフィールドを不変にできれば設計はシンプルになりますが、業務ロジック上、変更が前提となるフィールドもあります。判断の基準は次のとおりです。

フィールドの性質設計
作成後に変わらない値(識別子、座標、金額など)final + setter なし
業務上変更される値(在庫、ステータス、年齢など)final を付けず setter で検査

Person の場合、name は不変(final、setter なし)、age は可変(setter で検査)、という組み合わせの一例です。

カプセル化と不変・可変の選択を施した Person のコード全体は次のとおりです。

Person.java
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;
}
}

nameprivate final で setter なしの不変フィールド、ageprivate で setter による検査つきの可変フィールドです。Person のインスタンスを生成した後、name の書き換えはコンパイラーが禁止し、age の書き換えは setter 経由に統一されます。