0
0

More than 3 years have passed since last update.

Effective Java(第3版) 項目10 equalsをオーバーライドするときは一般契約に従う

Posted at

equalsをオーバーライドする場合に守るべき内容について、かなり詳細に記載しています。

記載内容

equalsのオーバライドが不要な場合

以下の条件のいずれかに該当する場合、equalsをオーバーライドしないのが正しい。

  • クラスの個々のインスタンスが本質的に一意(Threadクラス等)
  • 「論理的等価性」の検査を提供する必要が無い(正規表現のPattern等)
  • スーパークラスでオーバーライドしており、その振る舞いで問題ない(AbstractSetとSet等)
  • クラスがprivate、パッケージプライベートであり、equalsが呼ばれないことが確実

equalsのオーバライドが必要な場合

一般に、値クラス(Integer、Stringの様に、値を表現するクラス)の場合は、
equalsのオーバーライドが必要になる。
ただし、個々の値に対して1つのインスタンスしか存在しないクラスは
オーバーライド不要である。
(Objectのequalsメソッドが十分機能するため)

equalsメソッドの一般契約

equalsメソッドは、Objectクラスの使用に記載されている一般契約(反射性、対称性、推移性、一貫性、nullとの比較)に従う必要が有る。
(一般契約についての説明はObject.equals()の通りなのでここでは割愛)

反射性

この性質を意図せず破ってしまうことは少ない

対称性

この性質は、意図せずに破ってしまう可能性がある。
例として、大文字小文字を区別しない文字列クラスCaseInsensitiveStringを考えた場合、
equalsメソッドを以下の様に実装すると、対称性を破っている。

CaseInsensitiveString.java
private final String s; // 内部保持する文字列

@Override
public boolean equals(Object o) {
    if (o instanceof CaseInsensitiveString)
        return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
    if (o instanceof String)
        return s.equalsIgnoreCase((String) o);
    return false;
}

この場合、以下の様になる

CaseInsensitiveString cis = new CaseInsensitiveString("Foo");
String s = "foo";

System.out.println(cis.equals(s));            // true
System.out.println(s.equals(cis));            // false 対称性を破っている
System.out.println(List.of(cis).contains(s)); // 対称性を破っているため、結果は不明。OpenJDKではたまたまfalseになる

この問題を解決するには、CaseInsensitiveStringのequalsメソッドからStringクラスとうまく機能しようとしている部分を取り除くしかない。

推移性

この性質は、スーパークラスに新たな値要素を付加するサブクラスを考えると、容易に破ってしまう。
Point(点)とそのサブクラスColorPoint(色付きの点)の比較を考えた場合に、
以下のコードで、ColorPoint.equalsが色を無視した比較を行うと、対称性は守っているが、推移性を破ってしまう。

ColorPoint.java
@Override
public boolean equals(Object o) {
    if (!(o instanceof Point))
       return false;

    Point point = (Point) o;
    return x == point.x && y == point.y;
}
ColorPoint.java
@Override
public boolean equals(Object o) {
    if (!(o instanceof Point))
       return false;
    if (!(o instanceof ColorPoint))
       return o.equlas(this);  // 色を無視した比較

    // oはColorなので完全な比較
    return super.equals(o) && ((ColorPoint) o).color == color;
}

対称性、推移性は以下の様になる。

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

System.out.println(p1.equals(p2)); // true
System.out.println(p2.equals(p1)); // true 対称性は守れている
System.out.println(p2.equals(p3)); // true
System.out.println(p1.equals(p3)); // false 推移性が守れていない

これは、オブジェクト指向言語における同値関係の基本的な問題であり、
インスタンス化可能なクラスを拡張して、equals契約を守ったまま値要素を追加する方法ない。

この問題の回避方法は項目18「継承よりもコンポジションを選ぶ」ことである。
ColorPointがPointを拡張するのをやめ、ColorPointにPoint、Colorのフィールドを持たせ、
以下の様な、ColorPointの位置のPointを返すビューメソッドを返すようにすれば良い。

ColorPoint.java
  public Point asPoint() {
    return point;
  }

整合性

この性質は、信頼性のない資源に依存するequalsメソッドを記載した場合に、破ってしまう可能性がある。
例として、java.net.URLクラスのequalsメソッドは、整合性を破っており、呼び出すたびに結果が異なる可能性がある。
これは、URLクラスのequalsメソッドは、URLに関連付けられたIPアドレスの比較に依存しており、
ホストのIPアドレスへの変換は常に成功するとは限らず、同じ結果になることも保証されないためである。
しかし、互換性を維持するために、この誤った振る舞いを変更することが出来ずにいる。

非null性(名前が無いために命名)

o.equals(null)==trueとなる、誤った実装が行われることは考えづらいが、
誤ってNullPointerExceptionがスローされることは考えられる。
一般契約では、引数がnullの場合にNullPointerExceptionのスローは認められていないため、falseを返す必要が有る。

考察

長い項目ですが、記載されている内容はequalsに関する基本的な内容であり、
特に一般契約については理解しておく必要が有ります。

equalsのオーバライド有無

実はequalsを実装するかで悩ましいのは、既存のクラスにequalsメソッドを追加してしまってよいか、
というケースだと思っています。
(この項目の記載とは少しずれますが・・・)

特に、既存のクラスでjUnitが不十分だった場合に、比較を行いたいクラス(主にメソッドの戻り値)にequalsを追加したくなりますが、
追加した場合に、他の処理に影響が無いか、というのが確認しづらいです。
(さらに、equalsを追加する場合はhashcodeも実装するわけで、問題ないかが不安になります・・・)
やはり、値クラスに対しては、使用有無に関係なく、equalsとhashcodeは初期から実装しておくべきと思います。

使用しないコードは記載するべきではない、という意見もあると思いますが、
意図せずObject.equalsが使用されているかもしれず、equalsを実装して良いか分からない、
という状況になるより良いはずです。

一般契約

一般契約で主に問題になるのは、継承と推移性の問題と思います。
やはり、値クラスは継承するべきではない、ということになると思います。
(継承用に定義した、abstractの値クラスの継承は除きますが)
さらに一般化して、継承用に定義した抽象クラス以外は、継承を行うべきではないということになると思います。

equalsの実装

現在は、IntelliJ、Eclipse等のIDEで、equalsメソッドとhashcodeメソッドをセットで生成してくれますので、
これに従うのが良いと思います。
もし、何らかの理由で特殊な比較を行う場合は、その旨をコメントで記載するべきです。

さらに、RecordがJavaに導入されれば、値クラスに対してequalsの記載が不要にあり、
equals、hashcodeに対して、決まり切ったコードを記載する煩わしさが減るはずです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0