1. 導入: なぜ「推移律」が重要なのか
Javaで独自のクラスを定義する際、`equals()` メソッドをオーバーライドすることはよくあります。しかし、適当に実装すると「コレクションに入れた要素が見つからない」「ソートが正しく行われない」といった深刻なバグを引き起こします。その中でも、特に見落とされがちなのが「推移律(Transitive property)」です。「x.equals(y)` が真で、`y.equals(z)` が真なら、`x.equals(z)` も真でなければならない」というこの原則を守ることは、堅牢なシステム構築の第一歩です。
2. 基礎知識: equalsの5つの契約
`Object.equals()` をオーバーライドする際は、以下の5つの契約を守る必要があります。
・反射律: x.equals(x) は true
・対称律: x.equals(y) なら y.equals(x) も true
・推移律: x.equals(y) かつ y.equals(z) なら x.equals(z) も true
・整合律: 何度呼び出しても結果は同じ
・非null性: x.equals(null) は false
推移律を破る典型的なケースは、継承関係にあるクラスで「親クラスのフィールド+子クラスのフィールド」を比較しようとして、`instanceof` によるチェックを甘くしてしまうことです。
3. 実装/解決策: 推移律を守るための設計
推移律を守るための鉄則は、「継承よりもコンポジション(委譲)」を使うことです。もし継承を使う必要がある場合は、`getClass()` を用いて厳密にクラスを比較するか、あるいは `instanceof` を使う場合でも、比較対象の型を厳格に制限する必要があります。
4. サンプルプログラム
以下は、推移律を考慮した `Point` クラスの例です。
import java.util.Objects;
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
// 1. 自分自身との比較
if (this == o) return true;
// 2. nullチェックとクラス型の厳密な比較
// 推移律を守るため、異なるクラス同士はequalsで等価とみなさないのが安全
if (o == null || getClass() != o.getClass()) return false;
// 3. フィールドの比較
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
// equalsをオーバーライドしたら必ずhashCodeもセットで実装する
return Objects.hash(x, y);
}
}
5. 応用・注意点: 現場で陥りやすい罠
現場で最も注意すべきは、`instanceof` を使った比較です。例えば、`ColorPoint extends Point` を作成し、`equals` 内で `instanceof Point` を使って比較を行うと、`Point` と `ColorPoint` を混ぜた際に推移律が崩れやすくなります。
回避策のヒント:
もし「型が違っても値が同じなら等価としたい」という要件がある場合は、`equals` をオーバーライドしてはいけません。代わりに `isEqual(Object o)` のような独自のメソッドを定義するか、比較ロジックを別のクラス(Comparatorなど)に分離してください。
また、Java 16以降であれば `record` を利用することで、`equals` や `hashCode` の実装を自動化し、これらのバグを未然に防ぐことができます。可能な限り `record` の利用を検討してください。

コメント