Effective Java 第3版を読んだので、本書で紹介されていた全90項目のプラクティスを簡潔にまとめる。
https://www.maruzen-publishing.co.jp/item/?book_no=303408

本記事は第3章「すべてのオブジェクトに共通のメソッド」の項目について記載する。

第3章 すべてのオブジェクトに共通のメソッド

Objectクラスは拡張されるために設計されている。

以下のメソッドはオーバーライドするように設計されており、明示的な一般契約を持っている。

  • equals
  • hashCode
  • toString
  • clone
  • finalize

一般契約に従うことはこれらのメソッドをオーバーライドするクラスの責任である。

項目10 equalsをオーバーライドするときは一般契約に従う

equalsメソッドは同一オブジェクトをを参照しているか判定するメソッドである。

故に、クラスが論理的等価性という概念を持っていて、保持している値を比較する場合にオーバーライドすべきである。

equalsメソッドをオーバーライドする場合、以下の5つの条項を遵守する。

  • 反射性:nullでないxに対して、x.equals(x)はtrueを返さなければならない。
  • 対称性:nullでないxとyに対して、y.equals(x)がtrueのときのみ、x.equals(y)もtrueを返さなければならない。
  • 推移性:nullでないxとyとzに対して、x.equals(y)よy.equals(z)がtrueを返す場合、x.equals(z)もtrueを返さなければならない。
  • 整合性:nullでないxとyに対して、x.equals(y)の複数回の呼び出しは、一貫して同じ結果を返さなければならない。
  • nullでないxに対して、x.equals(null)はfalseを返さなければならない。

また、equalsをオーバーライドするときは、常にhashCodeをオーバーライドするべきである。(項目11)

これらの条項を遵守しつつ、高品質のequalsメソッドを作成するレシピは以下のとおり。

  1. 引数が自分自身のオブジェクトへの参照であるか==演算子を使い比較する。
  2. 引数が正しい型であるかinstanceof演算子を使い比較する。
  3. 引数を正しい型にキャストする。
  4. クラスの意味のあるフィールドが一致するか比較する。
class Human {
    String name;
    
    Human(String name){
        this.name = name;
    }
    public boolean equals(Object object) {
        if (object == this) { // 1
            return true;
        }
        if (!(object instanceof Human)) { // 2
            return false;
        }
        Human human = (Human)object; // 3
        return this.name.equals(human.name) ;// 4
    }
}

上記のようなコードを書いてテストするのは面倒なので、GoogleのAutoValueフレームワークを使うことが良い方法である。
1つのアノテーションを書くだけで以下のメソッドが自動生成されるので非常に簡潔である。

  • equals
  • hashcode
  • toString

項目11 equalsをオーバーライドするときは、常にhashCodeをオーバーライドする

equalsメソッドをオーバーライドしているクラスでは、hashCodeもオーバーライドしなければならない。
オーバーライドしない場合は、hashCodeの一般契約を破ることになるので、HashMapやHashSetなどのコレクションクラスが適切に機能しなくなる。

hashCodeメソッドの契約は以下の通り。

  • 同一オブジェクトのhashCodeメソッドが複数回呼び出された場合、hashCodeメソッドは常に同じ整数値を返さなければならない。
  • 二つのオブジェクトがequalsメソッドで等しければ、二つのオブジェクトのhashCodeメソッドは同じ整数値を生成しなければならない。
  • 二つのオブジェクトがequalsメソッドにより等しくなければ、二つのオブジェクトのhashCodeメソッドは必ずしも異なる整数値を生成しなければならないわけではない。ただし、異なる整数値を生成することでハッシュテーブルのパフォーマンスを改善する可能性があることを留意しなければならない。

hashCodeのオーバーライドを怠ると、「二つのオブジェクトがequalsメソッドで等しければ、二つのオブジェクトのhashCodeメソッドは同じ整数値を生成しなければならない。」の契約が破られてしまう。

その結果、HashMapやHashSetなどのハッシュバケットを扱うクラスでは意図しない動作をする。

この問題の解決はhashCodeメソッドをオーバーライドすることであり、equalsメソッドと同様にGoogleのAutoValueフレームワークを使うことが良い方法である。

項目12 toStringを常にオーバーライドする

ObjectクラスのtoStringメソッドが返す文字列は、一般的にクライアントが望んでいる内容ではない。
その文字列の形式は、クラス名 + アットマーク(@) + ハッシュコードの符号なし16進数 というもの。

toStringメソッドの一般契約は、「簡潔で人が読みやすい表現」となるべきと述べられている。

優れたtoStringメソッドを実装することでクラス自身が使いやすくなるだけではなく、システム自体のデバッグに大きく役立つ。
そのために、toStringメソッドではオブジェクトに含まれる意味のある情報をすべて出力するべきである。

ただし、staticなユーティリティクラスやenum型に対してはtoStringメソッドを書いても得られるメリットが少ないので書くべきではない。

上記をまとめると、インスタンス化可能なクラスでスーパークラスがtoStringメソッドをオーバーライドしていない場合は、toStringメソッドをオーバーライドするべきである。

項目13 cloneを注意してオーバーライドする

(詳細は追って追記する。)

コピーの機能はコピーコンストラクタかコピーファクトリで提供するべきである。
また、配列についてはcloneメソッドでコピーされるのが最善である。

項目14 Comparableの実装を検討する

ComparableはインターフェースでありcompareToメソッドを唯一のメソッドとして持っている。
Comparableを実装することで、インスタンスが自然な順序を持つことをクラスが示すことができる。

equalsメソッドと異なり、compareToメソッドはクラス間で機能する必要は無い。
そのため、compareToメソッドはClassCastExceptionをスローすることは許容されている。

equalsメソッドの契約の条項は以下の通り。

  • x > yがtrueであるとき、y > xはfalseとなること。
  • x > yかつ、y > zであるとき、x > z となること。
  • x = yがtrueであるとき、y = xはtrueとなること。

これらは、equalsメソッドの契約と同じ性質を持つ。(反射性、対称性、推移性)

public class Human implements Comparable<Human>  {
    
    private Integer age;

    public Human(Integer age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    @Override
    public int compareTo(Human human) {
        return Integer.compare(
                this.age,
                human.getAge());
    }
}

Java7では、staticのcompareメソッドが基本データクラスに追加されたため、compareToメソッド内では関係演算子を使うことは冗長なので非推奨である。