Java | オブジェクト指向:Entity の同一性

Java Java
スポンサーリンク

Entity の「同一性」とは何か

Entity の同一性とは
「そのオブジェクトが“誰なのか”を決める軸」
のことです。

もっと噛み砕くと、

・中身(名前や住所)が変わっても「同じ人」として扱いたいもの
・「同じデータを持っているか」ではなく「同じ ID を持っているか」で同一とみなすもの

これが Entity です。

逆に、金額や座標のように「中身が同じなら同じもの」として扱うのは値オブジェクト(Value Object)です。
ここをしっかり分けて考えるのが、オブジェクト指向の大きなポイントです。


「同じ人」だけど「状態は変わる」という感覚

現実世界でイメージしてみる

たとえば、「山田太郎さん」という人を考えます。

・引っ越せば住所が変わる
・転職すれば会社が変わる
・結婚すれば名字が変わることもある

でも、私たちは直感的に「同じ山田太郎さん」だと認識しています。
生年月日や住民票の番号など、何かしらの「本人を識別する ID」があるからです。

プログラムの Entity も同じで、

User の名前やメールアドレスが変わっても
Order の金額や状態が変わっても

「この ID を持っている限り同じ Entity」として扱います。

値オブジェクトとの違い

金額 1000円 を考えます。

1000円1000円 は、どっちがどっちでも構いません。
「どの 1000円か」は気にしないですよね。
中身(1000)さえ同じなら、同じ値として扱えます。

これが値オブジェクトです。

Entity は「誰か」、値オブジェクトは「いくら」「どれくらい」です。


Java で Entity の同一性を表現する

ID クラスを使った例

典型的な例として、注文(Order)を考えます。

final class OrderId {
    private final String value;

    OrderId(String value) {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("id は必須");
        }
        this.value = value;
    }

    String value() {
        return value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof OrderId)) return false;
        OrderId other = (OrderId) o;
        return this.value.equals(other.value);
    }

    @Override
    public int hashCode() {
        return value.hashCode();
    }
}
Java
final class Order {

    private final OrderId id;
    private String customerName;
    private int amount;

    Order(OrderId id, String customerName, int amount) {
        this.id = id;
        this.customerName = customerName;
        this.amount = amount;
    }

    OrderId id() {
        return id;
    }

    // 顧客名や金額は変わるかもしれない
    void changeCustomerName(String newName) {
        this.customerName = newName;
    }

    void changeAmount(int newAmount) {
        this.amount = newAmount;
    }
}
Java

ここでは「同じ Order かどうか」は OrderId で判定します。
名前や金額が違っていても、id が同じなら「同じ注文」とみなします。

equals / hashCode を ID で決める

Order 自体に equals を定義することもよくあります。

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Order)) return false;
    Order other = (Order) o;
    return this.id.equals(other.id);
}

@Override
public int hashCode() {
    return id.hashCode();
}
Java

これで、

Order o1 = new Order(new OrderId("ORD-001"), "山田", 1000);
Order o2 = new Order(new OrderId("ORD-001"), "佐藤", 2000);

o1.equals(o2); // true
Java

となります。

「顧客名や金額が違っていても、ID が同じなら同じ注文として扱う」
これが Entity の同一性の考え方です。


Entity と値オブジェクトの同一性の違い(重要)

値オブジェクトは「中身」で決める

金額 Money を値オブジェクトとして作るとします。

final class Money {
    private final int amount;

    Money(int amount) {
        this.amount = amount;
    }

    int amount() {
        return amount;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money)) return false;
        Money other = (Money) o;
        return this.amount == other.amount;
    }

    @Override
    public int hashCode() {
        return Integer.hashCode(amount);
    }
}
Java

この場合、

Money m1 = new Money(1000);
Money m2 = new Money(1000);

m1.equals(m2); // true
Java

となります。
「どの 1000 円か」ではなく「金額が 1000 なら同じ」とみなすからです。

Entity は「ID」で、値オブジェクトは「値そのもの」で

整理すると、

Entity
→ 同一性は ID で決める
→ 名前や状態が変わっても、ID が同じなら同じとみなす

値オブジェクト
→ 同一性は中身の値で決める
→ 別インスタンスでも値が同じなら同じとみなす

という違いがあります。


よくある落とし穴と注意点(深掘り)

1. equals を「全部のフィールド」で比較してしまう

Entity で、こんな equals を書いてしまうことがあります。

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Order)) return false;
    Order other = (Order) o;
    return this.id.equals(other.id)
        && this.customerName.equals(other.customerName)
        && this.amount == other.amount;
}
Java

一見丁寧に見えますが、これは Entity というより「値オブジェクトのノリ」です。

注文の金額や名前が変わるたびに「別の注文」になってしまい、
Set<Order>Map<Order, ...> に入れたときに不整合が起きます。

Entity は「同一性を ID だけで決める」ことが多いです。
フィールド全部を equals に入れるのは慎重に考えたほうがいいです。

2. 生まれたて(新規作成)Entity の ID をどう扱うか

現実のコードでは、「まだ DB に保存していない Entity」には ID がないことも多いです。

Order newOrder = new Order(null, "山田", 1000);
Java

このとき equals / hashCode をどうするかは悩ましいポイントですが、
初心者のうちは「ID がついてから equals を使う」と割り切ってしまうのもアリです。

あるいは、

・集約の中では ID なしの新規オブジェクトはコレクションで単純に扱う
・永続化後に ID が振られたら equals/hashCode の意味が安定する

といった設計に寄せていくこともあります。

最初から完璧を目指すより、「ID が同じなら同じ Entity」という軸だけしっかり持っておけば十分です。


Java の参照比較(==)との違い

== は「同じインスタンスかどうか」

Java の == は、「同じインスタンス(同じメモリアドレス)かどうか」を見ています。

OrderId id = new OrderId("ORD-001");
Order o1 = new Order(id, "山田", 1000);
Order o2 = o1;

(o1 == o2);     // true(同じインスタンス)
Java

しかし、別の場所で同じ ID から別インスタンスを作ることがあります。

Order o3 = repository.findById(new OrderId("ORD-001"));
Java

多くの場合、o3o1 とは別インスタンスです。
でも「ビジネス的には同じ注文」です。

ここで == を使うと false になってしまうので、
「同じ Entity かどうか」を見るときは equals(=ID 比較)を使うのが基本です。


まとめ:Entity の同一性を意識すると何が変わるか

Entity の同一性を意識できるようになると、

どのクラスが「人」や「注文」のような存在で
どのクラスが「金額」や「期間」のような値なのか

が、はっきり分けられるようになります。

その結果、

equals/hashCode をどう定義するか
コレクションに入れたときどう振る舞うべきか
どこまで変更しても「同じもの」とみなすか

といった設計の判断が、ブレなくなっていきます。

タイトルとURLをコピーしました