やり直しJava

Object

Jump to Section

java.lang.Objectクラスは 全てのクラスやインタフェースのスーパークラスになります。この章では 生成から消滅までのオブジェクトのライフサイクルに関連した内容を見ていきます。

Objectクラスは いくつかの基礎となるメソッドを持っていて、サブクラスでオーバーライドする場合は メソッドの一般契約に従う必要があります。それらのメソッドを適切に実装する方法について見ていきます。

オブジェクトのライフサイクル

オブジェクトの生成

通常オブジェクトを生成するにはnew演算子を用います。また、リフレクションでオブジェクトを生成することもできます。newやリフレクションで 必要な時に必要なだけオブジェクトを生成することができますが、時には 外部からオブジェクトを生成するのを制限したり、オブジェクトの数を制限したい場合もあります。ここでは、オブジェクトの生成を制御する技法などについて見て行きます。

staticファクトリメソッド

staticファクトリメソッドは オブジェクトの生成を制御するための技法です。publicなコンストラクタを提供する代わりに、オブジェクトを生成するためのpublicでstaticなファクトリメソッドを提供します。大抵コンストラクタを privateまたはパッケージprivateとすることとセットで適用します。この技法には多くのメリットがあります。

  • オブジェクトの生成を制御することができます。例えば 生成できるオブジェクトの数を1に制限するとシングルトンパターンを実現することができます。また、生成したオブジェクトをキャッシュしておき、同等なオブジェクトの生成依頼を受けた場合に キャッシュしてあるオブジェクトを返すようなキャッシュ機構を実現することもできます
  • コンストラクタの引数で 様々な種類のオブジェクトを生成するような場合に、名前を付けることによってコンストラクタよりも意味を明瞭にすることができます。例えば、new Color(255, 0, 0)とするよりも、createRed()というstaticファクトリメソッドを用意した方が意味が明瞭になります。
  • staticファクトリメソッドでは、戻り値の型の任意のサブタイプを返すことができます。戻り値として返すオブジェクトはpublicなクラスである必要もなく、後から新たに作成したクラスに差し替えることもでき、生成するオブジェクトの型に対する柔軟性を提供します

staticファクトリメソッドにはメリットが沢山ありますが、デメリットもあります。JavaDocを生成した場合に コンストラクタと違って他のメソッドの中に紛れてしまい、ユーザに見落とされてしまいがちです。そのためstaticファクトリメソッドには次のような慣習的な命名則があります。

staticファクトリメソッドの慣習的な命名則
メソッド名説明
of引数で受け取った要素を持つコンテナ型のオブジェクトを返します。List  List.of()、
Set  Set.of()、
Map  Map.of()
valueOf受け取った引数を 該当のクラスに変換したオブジェクトを返します。Integer  Integer.valueOf()、
String  String.valueOf()
fromvalueOfと同様。Date  Date.from()
getInstance引数に対応するオブジェクトを返します。引数がない場合はデフォルトを返します。Calendar  Calendar.getInstance()
newInstance
createInstance
getInstanceと似ていますが、呼び出しのたびにオブジェクトを生成します。Object  Array.newInstance()
getXXXgetInstanceと似ていますが、ファクトリメソッドが別のクラスに属している場合です。Path  FileSystem.getPath()
newXXX
createXXX
newInstanceと似ていますが、ファクトリメソッドが別のクラスに属している場合です。Reader  Channels.newReader()

Builderパターン

Builderパターンは フィールドを沢山持つようなオブジェクトに対して、生成時にそれらのフィールドを初期化するような場合に適用できる技法です。フィールドを沢山持つようなオブジェクトを生成する際に適用できる技法として、テレスコーピング・コンストラクタJavaBeansパターンBuilderパターンがあり、Builderパターンが最も有用とされています。

また、Javaには名前付き引数の機能が無いため、コンストラクタ以外でも 名前付き引数の代替としてBuilderパターンが利用されることもあります。

テレスコーピング・コンストラクタ

Javaではデフォルト引数を指定することができないため、デフォルト値を持つフィールドがある場合は、次のようなテレスコーピング・コンストラクタが良く用いられます。(「テレスコープ」は「順々に嵌め込む」の意味。)

private int a;
private int b;
private String c;

// デフォルト値 b=0, c="default"
public SomeClass(int a) {
    this(a, 0);
}
// デフォルト値 c="default"
public SomeClass(int a, int b) {
    this(a, b, "default");
}
// デフォルト値 b=0
public SomeClass(int a, String c) {
    this(a, 0, c);
}
// 全ての引数を指定
public SomeClass(int a, int b, String c) {
    this.a = a;
    this.b = b;
    this.c = c;
}

テレスコーピング・コンストラクタは フィールドの数が多くなってくると 次のような問題が出てきます。

  • フィールドの数やデフォルト値のバリエーションが多くなってくると、それだけ多くのコンストラクタを用意しなくてはいけなくなります。
  • フィールドの数が多くなってくると、コードを書くのも読むのも大変になってきます。それに伴い、引数の順番を間違えることによるバグを引き起こし易くなります。

フィールドの数が多いような場合には使いたくないパターンです。

JavaBeansパターン

フィールドを沢山持つオブジェクトを初期化する場合に適用できる別のパターンがJavaBeansパターンです。コンストラクタ自体は引数なしとして、各フィールドのセッターを提供するパターンです。

このパターンであれば、必要なフィールドのセッターだけを呼び出せばよく、順番を気にする必要もありません。しかし、このパターンには次のような欠点があります。

  • 必要なフィールドの設定が漏れてしまい 不完全な状態のオブジェクトを使用することによるバグを引き起こす可能性があります。
  • フィールドをfinalにすることができないため、不変クラスにすることができなくなってしまいます。

使い勝手は良いのですが、ユーザが間違った使い方をしてしまうことを防げないことが、このパターンの最大の欠点になります。

Builderパターン

Builderパターンは JavaBeansパターンの使い勝手の良さを提供しつつ、JavaBeansパターンの欠点を補うようなパターンです。オブジェクトを生成するためのBuilderを生成し、メソッドチェーンで任意の属性を指定した後 build()メソッドで目的のオブジェクトを生成するパターンです。

// Person クラス
public class Person {
    private final String name;
    private final int age;
    private final int numOfSibling;
    private final int numOfChildren;

    // Person ビルダ
    public static class PersonBuilder {
        
        // 必須パラメータ
        private final String name;
        private final int age;

        // オプショナルパラメータ
        // デフォルト値で初期化
        private int numOfSibling = 0;
        private int numOfChildren = 0;
        
        // コンストラクタでは必須パラメータを設定
        public PersonBuilder(String name, int age) {
            this.name = name;
            this.age = age;
        }

        // オプショナルパラメータに対してはセッターを提供。
        public PersonBuilder numOfSibling(int numOfSibling) {
            this.numOfSibling = numOfSibling;
            return this;
        }

        public PersonBuilder numOfChildren(int numOfChildren) {
            this.numOfChildren = numOfChildren;
            return this;
        }
        
        public Person build() {
            return new Person(this);
        }
    }
     
    // コンストラクタはprivateに。
    prvivate Person(PersonBuilder builder){
        this.name = builder.name;
        this.age = builder.age;
        this.numOfSibling = builder.numOfSibling;
        this.numOfChildren = builder.numOfChildren;
    }
}
 
// 利用例
public static void main(String[] args) {
    Person bob = new PersonBuilder("Bob", 12)  // 必須パラメータ
            .numOfSibling(2)                   // オプショナルパラメータの指定は任意
            .build();                          // 最後に build()
}

JavaBeansパターン同様、任意のセッターを任意の順番で呼び出すことができ、かつ生成するオブジェクトを不変オブジェクトにすることができます。また、上の例のPersonクラスのコンストラクタで 不完全なオブジェクトでないかどうかや 不変式が崩れていないかどうか等のチェックを行うこともできます。

Builderパターンは Builderと生成するオブジェクトのセットを継承させて拡張することができます。しかし、サブクラスのBuilderにメソッドを追加しても そのままではメソッドチェーンに組み込むことができません。そのため、Builderを継承させる場合には 擬似自分型のイディオムにする必要があります。詳しくは「総称型」の章の「擬似自分型」を参照してください。

オブジェクトの複製

CloneableインタフェースとObject.clone()メソッド

オブジェクトの複製をできるようにしたい場合、CloneableインタフェースとObjectのclone()メソッドを利用することが選択肢の一つとして挙げられます。しかし、CloneableインタフェースとObject.clone()メソッドの仕組みは意外と複雑で、仕組みを理解していないと思わぬバグを引き起こすことにもなりかねません。この章では その仕組みを説明すると共に 利用する際の注意点などをまとめていきます。

CloneableインタフェースとObject.clone()メソッドの仕組み

まず大事な点とその背景を3つ挙げます。

  • Objectはclone()メソッドを持っていますが Cloneableインタフェースを実装していません。
    Objectは全てのインタフェースのスーパータイプでもあるので 当然と言えば当然です。また、複製を許可したくないクラスもあるので、ObjectがCloneableインタフェースを実装しないのは妥当です。
  • そもそもCloneableにはclone()メソッドが定義されていません。
    これはどうも設計ミスのようです。(「Java Design Issues – The clone Dilemma」)
  • Object.clone()メソッドはprotectedで 外部から呼び出すことはできません
    複製を許可したくないクラスもあるので、protectedとして実装しておいて 複製が必要なクラスは適宜publicとしてオーバーライドするという設計方針は妥当と考えられます。

Cloneableインタフェースにはclone()メソッドが定義されているわけではないので、Object.clone()メソッドは抽象メソッドを実装したわけではありません。しかし、CloneableインタフェースとObject.clone()メソッドが全く関係ないかという言うと そういうわけでもありません。Object.clone()メソッドはnativeメソッドですが、JavaDocに実装の詳細が次のように説明されています。

Object.clone()メソッドは Objectから派生したクラスがCloneableを実装していなければCloneNotSupoortedExceptionを投げ、そうでなければフィールドをシャローコピーした そのクラスの別のインスタンスを返します。

Effective Javaの中でも述べられていますが、このCloneableインタフェースの使われ方は異常で、真似すべきではないものです。

この事実からもう一つ言えることは、あるクラスでclone()メソッドをオーバーライドする場合、Object.clone()を呼び出さないのであればCloneableインタフェースを実装する必要がないということです。ただし、サブクラスが作成されるクラスの場合はObject.clone()を使う方が便利です。

Cloneableインタフェースはclone()メソッドを持っておらず、Object.clone()もprotectedであるため、CloneableやObjectの変数を介して 外部からclone()メソッドを呼び出すことができません。外部からclone()メソッドを呼び出すには、publicなclone()メソッドをオーバーライドしている具体的なクラスを指定するか、publicなclone()メソッドを定義したインタフェースを別途用意する必要があります。リフレクションで呼び出すこともできますが、publicなclone()メソッドを持っていない場合は呼び出しが失敗します。

Object.clone()メソッドをオーバーライドする際のガイドライン

clone()メソッドをオーバーライドする際の おおまかなガイドラインを見ていきます。

Object.clone()メソッドがそのまま使えるケース

クラスの全てのフィールドが プリミティブ型か不変オブジェクトへの参照型の場合は、オブジェクトの複製にスーパークラスであるObjectクラスのclone()をそのまま使うことができます。サブクラスでは、次の3点を実装する必要があります。

  • Cloneableインタフェースを実装する必要があります。Cloneableインタフェースを実装しないと Object.clone()がCloneNotSupportedExceptionを投げてしまいます。
  • Object.clone()を呼び出す際に、CloneNotSupportedExceptionに対する例外処理コードを書く必要があります。Cloneableインタフェースを実装していれば発生し得ないため、必要のないコードを書くことになります
  • Object.clone()の戻り値がObjectであるため、自身のクラスにキャストします

Object.clone()を呼び出さずに コンストラクタで自身と同じクラスのオブジェクトを生成することもできます。しかし そのようにすると、そのクラスのサブクラスが作成された場合、サブクラスからObject.clone()を呼び出すと サブクラスのオブジェクトではなく 親クラスのオブジェクトが返ってしまうため注意が必要です。Object.clone()を呼び出せば 実行時のthisのクラスに応じたオブジェクトを返してくれるので便利です。

class Super implements Cloneable {
    public Super clone() {
        try {
            // return new Super();  // サブクラスが作成される場合は、コンストラクタは使えない。
            return (Super) super.clone();  // thisの実行時クラスのインスタンスが返る。
        } catch(CloneNotSupportedException e) {
            // 発生しない
            throw new AssertionError();
        }
    }
}

class Sub extends Super {
    public Sub clone() {
        return (Sub) super.clone();  // Subクラスのインスタンスが返る。
        // Super.clone()でコンストラクタを使ってインスタンスを生成するとSuperクラスのインスタンスが返ってしまい、
        // ClassCastExceptionが発生してしまう。
    }
}
Object.clone()メソッドで取得したオブジェクトに追加処理が必要なケース

Object.clone()はシャローコピーを行うため、クラスのフィールドが可変オブジェクトへの参照型の場合は 個別にディープコピーを行う必要があります。そうしないと複製元と複製先で同じ可変オブジェクトを参照してしまうためです。ディープコピーに際して、2点気をつけるべき事項があります。

  • Object.clone()で取得したオブジェクトのfinalフィールドは 後から変更することができません。そのため、個別にディープコピーを行うフィールドは finalにすることができません
  • フィールドの可変オブジェクトを再帰的に複製するだけでは十分でない場合もあります。例えば フィールドの参照先の可変オブジェクトが木構造になっている場合、ディープコピーによって その木構造ごと複製を作成します。しかし、要素同士が木構造とは別の参照関係を持っている場合は それらの参照を全て更新しないといけません。そうしないと、複製先の木構造の要素が複製元の木構造の要素を参照してしまい、おかしなことになってしまいます。

ディープコピーが必要なフィールドを持つクラスを不変クラスにしたい場合は、Object.clone()の代わりに 次に紹介するコピーコンストラクタを用います。

Object.clone()に代わる複製技法
コピーコンストラクタ

CloneableインタフェースとObject.clone()メソッドは、仕組みを理解して適切に実装する必要があるにも関わらず、コンパイラがそれを強制できないため 誤りを誘発し易い仕組みと言えます。

CloneableインタフェースとObject.clone()に代わる複製技法として、コピーコンストラクタが挙げられます。コンストラクタにそのクラスのオブジェクトを渡して、複製を生成するコンストラクタです。コピーコンストラクタでは 引数としてそのクラスのサブクラスのオブジェクトを受け取ることもできます

また、そのクラスがあるインタフェースを実装していて、そのインタフェースを介して複製を作成するのに必要十分な情報が取得できるのであれば、引数をインタフェースにして 受け取れるオブジェクトの型を更に広くすることもできます。Collectionの実装クラスに そのようなコピーコンストラクタを用意しているクラスがいくつかあります。例えばArrayListは次のようなコピーコンストラクタを用意しています。

public ArrayList(Collection c)

ArrayListだけに留まらず 任意のCollectionを引数に受け取ることができます。この方法であれば、可変オブジェクトへの参照フィールドがfinalの場合でも問題なく対応することができます。

しかし、コピーコンストラクタが使えない場面もあります。例えば、オブジェクトを受け取って複製を作成するようなメソッドにおいて、引数が具体的なクラスではなくインタフェースであり、引数のオブジェクトと同じ実装クラスの複製を作成しなければならないような場合です。受け取ったオブジェクトの実装クラスを特定できない場合は、どのクラスのコピーコンストラクタを呼べばよいのか特定することができません。

static <E> List<E> createClone(List<E> list) {

    // コピーコンストラクタは実装クラスを指定する必要があるため、
    // 実装クラスが特定できる必要がある。
    List<E> ret = new ArrayList<>(list);
    return ret;
}

このような場合は、引数の実装クラスを知らなくても clone()メソッドを呼ぶだけで良いので、Object.clone()の仕組みの方が適しています。(ただし、引数のインタフェースでclone()メソッドを提供しないと、リフレクションを利用しない限りclone()メソッドが呼び出せないため注意が必要です。)

// CloneableListではpublicなclone()メソッドを定義しているとする。
static <E> CloneableList<E> createClone(CloneableList<E> list) {
    return list.clone();
}

オブジェクトの消滅

ファイナライザ(Java SE9~ 非推奨)

Javaではファイナライザの機能を利用して、オブジェクトが削除される際に必要な処理を実装することができます。簡単に組み込める一方で、仕組みを理解しないで使ってしまうと リソースの枯渇などの深刻なトラブルを招きかねず、実は使い方が難しい機能です。そのような背景もあり、Java SE9で ファイナライザ機能の中核となるfinalize()メソッドは非推奨になりました

ここでは、ファイナライザの仕組みと問題点を見ていきます。続いて、ファイナライザを有効に活用できる場面を見てみますが、ファイナライザは非推奨であるため 代替のクリーナについて見ていきます。

ファイナライザの仕組み

Object.finalize()をオーバーライドしたfinalize()メソッドはファイナライザと呼ばれます。finalize()はObjectクラスに定義されているため、全てのクラスがfinalize()を持ちます。しかし、finalize()が意味を持つのはObject.finalize()をオーバーライドした場合のみです。これは、Object.finalize()の実装が何もしないから意味を持たないということではなく、Object.finalize()をオーバーライドしたクラスと オーバーライドしていないクラスでオブジェクトのライフサイクルが異なることを意味します。ここでは便宜上 次のように呼ぶことにします。

  • ファイナライザを持つオブジェクト:Object.finalize()をオーバーライドしたクラスのオブジェクト
  • ファイナライザを持たないオブジェクト:Object.finalize()をオーバーライドしていないクラスのオブジェクト

ファイナライザを持たないオブジェクトは、参照されなくなった後 ガベージコレクタ(GC)が動作するとオブジェクトのメモリが解放されます。これに対して ファイナライザを持つオブジェクトは、参照されなくなった後GCが動作するとfinalize()メソッドが実行されます(ただし即座に実行される保証はありません)。そして finalize()メソッドが完了した後 GCが動作すると ようやくオブジェクトのメモリが解放されます。ファイナライザを持つオブジェクトは 参照されなくなってから少なくとも2回はGCが動作しないと メモリが解放されないことになります。これは、参照されなくなってから2回GCが動作すると必ずメモリが解放されるということではありません。世代別GCでOld領域にオブジェクトが存在する場合、参照されなくなってから 何度Scavenge GCが動作してもオブジェクトのメモリは解放されず、少なくともFull GCが2回動作する必要があります。また、GC動作中はfinalize()メソッドの実行は保留されてしまうため、GCが頻発するような状況ではfinalize()の実行が先延ばしされ、参照されなくなってから2回GCが動作してもメモリが解放されない場合が出てきます。

C++に慣れている人から見ると 一見するとファイナライザはデストラクタのように思えてしまうかも知れませんが、デストラクタとは掛け離れている別物であることに注意が必要です。C++のデストラクタは newで生成したインスタンスに対してはdeleteを呼び出す際に実行され、また ローカル変数などでスタックに生成されたインスタンスに対しては スコープを抜ける際に実行されます。つまり、デストラクタはアプリケーション側で呼び出しタイミングを制御することが可能です。これに対してJavaでは ファイナライザが呼び出されるタイミングはGCの動作に依存するため、基本的にアプリケーション側で制御することはできません。(System.gc()で明示的にFullGCを動作させることもできますが、基本的にはアプリケーションはGCの動作タイミングに関与しません。)

ファイナライザを理解する」(PDF:富士通)

リソース解放のためにファイナライザを使わない

オブジェクトが削除される際に必要な処理は リソースの解放に関連するものが多いにも関わらず、次の理由からファイナライザはリソースを解放する用途には向きません。

  1. アプリケーション側では基本的にファイナライザの実行タイミングを制御することができず、GCの動作タイミングに依存することになります。オブジェクトが不要になってからGCが動作するまでにはタイムラグが発生し、特に世代別GCでOld領域に入ってしまうと Full GCが動作するまでファイナライザの実行が保留されてしまいます。一般的にはFull GCは頻発しないようにチューニングされるので、Old領域に入ってしまったオブジェクトは、不要になってからファイナライザが実行されるまでに相当のタイムラグが発生する可能性があります。また、New領域のオブジェクトであっても GCが頻発するような状況ではファイナライザの実行が遅延する場合もあります。
    そのため、リソースの解放をファイナライザで行うと リソースの解放が追いつかず リソースが枯渇してしまう可能性があります
  2. ファイナライザの実行タイミングは 基本的にはGCの動作タイミングに依存しますが、GCの動作タイミングは概ねメモリの不足状況に依存します。そのため、ファイナライザでファイルを閉じたり DBコネクションをプールに戻したりといった メモリ以外のリソース解放を行う場合、メモリは余裕があるのでGCが動作せず それらのメモリ以外のリソースが枯渇してしまう可能性があります。これもファイナライザがリソース解放に向かない理由の1つです。
  3. 1番目で述べたようにファイナライザは実行が遅延される可能性もありますが、それどころかファイナライザが実行される前に プログラムが終了してしまう可能性さえもあります。そのため、ファイナライザの処理でファイルへの書き出しやDBレコード操作といった永続的なストレージ等への変更を行う場合、それらの処理が実行されないままになってしまう可能性があります。

リソースの解放を行うには、後で紹介するAutoCloseableを利用します。AutoCloseableを利用する場合、ユーザが明示的にリソース解放を行わないといけないケースがあり、その呼び出しが漏れた場合のセーフティネットとしてファイナライザを利用することができます。ただし、ファイナライザはJava SE9で非推奨となったため、その用途では代わりにクリーナを利用します。

ファイナライザが利用できるのは、実行の延伸が問題にならず、場合によっては実行されなくても困らないような用途に限定されます

ファイナライザの注意点

ファイナライザはJava SE9から非推奨となったため そもそも使うべきではないのですが、それ以前でも使用に当っては様々な注意点がありました。ファイナライザはリソース解放の用途に向かないだけではなく、利用する際には 次のような注意点を踏まえる必要がありました。

  1. ファイナライザは 対象オブジェクトがどこからも参照されなくなった後に実行されますが、ファイナライザの中で該当オブジェクトへの参照を復活させてしまわないように注意が必要です。ファイナライザの中でもthisで自身を参照することが可能です。thisをクラスフィールドに設定したり 他のオブジェクトに渡してしまうと オブジェクトへの参照が復活してしまい、メモリリークの原因になります。
  2. 不特定多数のユーザに公開する finalでないクラス(サブクラスの作成が可能なクラス)の場合、ファイナライザ攻撃の対象になり得ます。ファイナライザ攻撃とは、コンストラクタで例外が発生した場合に、オブジェクトの初期化が完了していないにも関わらず ファイナライザが呼ばれてしまう仕組みを悪用した攻撃です。ファイナライザ攻撃の具体的な例と対策についてはJPCERTのサイト()を参照してください。
    ファイナライザ攻撃を防ぐ1つの方法は、finalの何もしないfinalize()メソッドを定義することです。しかし、これによって信頼できないユーザのファイナライザ攻撃を防ぐことができる代わりに、信頼できるユーザがサブクラスを作成した場合に ファイナライザを利用してサブクラス独自の後処理をすることができなくなってしまいます(AutoCloseableで代替できます。後述)。
    ファイナライザ攻撃を防ぐ 別の方法は、スーパークラスのコンストラクタを呼び出す前に 検査を行い例外を発生させることです(Java SE 6以降適用可能)。詳しい説明はJPCERTのサイトを参照してください。この方法であれば、コンストラクタで例外が発生してもファイナライザが呼ばれないため、ファイナライザ攻撃を防ぐことができます。
  3. ファイナライザを実行するスレッドおよび実行順序はJVMに委ねられます。ファイナライザがどのような順番で実行されるかは不定であり、ファイナライザは単一のスレッドで実行されるかも知れないし、複数のスレッドで実行されるかも知れません。複数のオブジェクトのファイナライザが同一のデータを操作するような場合があれば 並行アクセスしても安全であるようにしなければいけませんし、デッドロックが発生しないように注意を払う必要があります。
ファイナライザの代替:リソース解放のためのAutoCloseable

オブジェクトが不要になった際に リソース解放などの後処理が必要な場合は、ファイナライザの代わりにAutoCloseableを利用します。AutoCloseableインタフェースを実装して close()メソッドで必要な後処理やリソース解放処理を行います。try-with-resource文で該当のオブジェクトを生成すれば、ブロックを抜ける際にclose()が自動的に呼び出されますし、オブジェクトを生成する箇所と削除する箇所が離れていてtry-with-resource文が使えないような場合には 明示的にclose()を呼ぶこともできます。これはC++において、スタックに生成したインスタンスは自動的にデストラクタが呼ばれ、ヒープに生成したインスタンスに対しては明示的にdeleteでデストラクタを呼び出すのに似ています。

C++では明示的なdelete漏れによりメモリリークが発生する可能性がありますが、次に説明するセーフティネットとしてのクリーナを併用することによって AutoCloseableのclose()の呼び出し漏れを防ぐこともできます。

ファイナライザの代替:セーフティネットとしてのクリーナ

前述の通り ファイナライザの仕組みはAutoCloseableのclose()呼び出しが漏れた場合のセーフティネットとして利用することができます。しかし、Java SE9からファイナライザは非推奨となり、代わりにJava SE9で導入されたjava.ref.Cleanerによるクリーナの仕組みを利用します。

JDK1.2の頃からjava.lang.refパッケージのWeakReference、PhantomReference、ReferenceQueueなどを使うことによって ファイナライザと同じような後処理の仕組みを実現することは可能でした。しかし、これらのクラスを使うにはガベージコレクタやファイナライザの仕組みを理解している必要があります。また キューを監視して参照レベルが変更になったら後処理を実行する監視スレッドを実装したり、Reference群の管理を実装する必要があります。マルチスレッドのvolatileと同様、柔軟な仕組みを実現できる道具ではありますが、高度な知識が必要で 取り入れるには少し敷居の高いものでした。

クリーナは それらの高度な知識や実装を隠蔽して ファイナライザと同じような後処理の仕組みを提供してくれます。ファイナライザと比べて 次のような利点が挙げられます。

  • ファイナライザを実行するスレッドはJVMに委ねられていましたが、クリーナの場合はクリーナを実行するスレッドはライブラリが管理しています。そしてCleaner生成時にThreadFactoryを指定することができ、独自のスレッドで置き換えることもできます
  • ファイナライザ攻撃対策をすると サブクラスでfinalize()メソッドをオーバーライドすることができませんが、クリーナの仕組みを利用して後処理を行うことができます。

ただし、クリーナの場合も 動作タイミングはGCの動作タイミングに依存するため、リソース解放の用途には使えませんクリーナはあくまでAutoCloseableのclose()が漏れた場合の セーフティネットのような用途に利用します。AutoCloseableとクリーナの仕組みを利用したクラスの例を次に挙げます。クリーナでは、クリーニングアクションと呼ばれる ファイナライザの役目をするstaticなメンバクラスを用意するのが典型的なパターンになります。

public class SomeObj implements AutoCloseable {
    // Cleanerは複数オブジェクトで共有できる。
    // create()でThreadFactoryを指定することもできる。
    private static final Cleaner cleaner = Cleaner.create();

    // Cleanableとクリーニングアクションはオブジェクトごとに持つ。
    private final Cleanable cleanable;
    private final SomeObjCleaner objCleaner;
    
    private final String name;
    
    public SomeObj(String name) {
        this.name = name;
        objCleaner = new SomeObjCleaner(name);
        cleanable = cleaner.register(this, objCleaner);
    }
    
    public void doTask() {
        System.out.println(name + ": do task.");
    }
    
    @Override
    public void close() {
        // 実際のクリーニング処理はクリーニングアクションクラスで実装する。
        cleanable.clean();
    }

    // クリーニングアクションクラス。メンバクラスにする場合は必ずstaticにする。
    // 対象オブジェクトへの参照は持ってはいけない。
    // クリーニングに必要な対象オブジェクトの情報は抜粋して持つようにする。
    private static class SomeObjCleaner implements Runnable {

        private final String name;
        public SomeObjCleaner(String name) {
            this.name = name;
        }
        
        @Override
        public void run() {
            // ここでクリーニング処理を行う。
            System.out.println(name + ": do cleaning.");                
        }
        
    }
}

上のクラスの利用例は次の通りです。

public static void main(String[] args) throws InterruptedException {

    // try-with-resource文が使える場合
    try(SomeObj obj1 = new SomeObj("obj1")){
        obj1.doTask();
    }
    
    // try-with-resource文が使えないような場合は、
    // 明示的にclose()を呼び出す。
    {
        SomeObj obj2 = new SomeObj("obj2");
        obj2.doTask();
        obj2.close();
        obj2.close(); // 2度目の呼び出しは何もしない。
    }
    
    // 明示的なclose()が忘れられた場合。
    {
        SomeObj obj3 = new SomeObj("obj3");
        obj3.doTask();
        // obj3 = null;
    }
    System.gc();
    TimeUnit.SECONDS.sleep(5);
}

基本的にはtry-with-resource文で利用し、try-with-resource文が利用できない場合は 明示的にclose()を呼び出します。close()の呼び出しが忘れられてしまった場合は GCのタイミングでクリーニングアクションが呼び出されます。上の例ではobj3を明示的にnullにしないとGCのタイミングでクリーニング処理は行われませんでしたが、この辺りの動作はJVMの実装によって差が出てくるかも知れません。

クリーナの仕組みを利用するに当っての注意点を次にまとめます。

  • クリーニングアクションクラス(例ではSomeObjCleaner)は対象オブジェクト(例ではSomeObj)への参照を保持してはいけません。参照を持ってしまうと いつまでたっても対象オブジェクトが到達不能と判断されないため、クリーニングアクションが呼ばれなくなってしまいます。
  • クリーニングアクションクラス(SomeObjCleaner)を対象オブジェクト(SomeObj)のメンバクラスにする場合、必ずstaticにする必要があります。staticにしないと暗黙的に対象オブジェクトへの参照を持ってしまうためです。(非staticなメンバクラスは暗黙的にエンクロージングクラスへの参照を持ちます。)
  • 大事なことなので繰り返しますが、ファイナライザ同様リソース解放の用途には向きません。AutoCloseableの解放漏れに対するセーフティネットのような、延伸が問題にならず、場合によっては実行されなくても困らない用途に限られます

メモリリーク

Javaではガベージコレクタ(GC)が 不要になったオブジェクトのメモリを自動的に解放してくれます。そのおかげで CやC++のようにメモリの解放に神経質にならなくても良くなりました。

しかし、Javaでもメモリリークは発生し得ます。不要になったオブジェクトへの参照が残り続けることが原因です。ここでは、メモリリークが発生し易いケースを3つ紹介します。

  • キューやスタックなどの実装で 独自にメモリ管理を行う場合。
  • キャッシュの仕組みを実装する場合。
  • リスナやコールバックを登録する場合。

それぞれ簡単に見ていきます。

キューやスタックなどの実装で 独自にメモリ管理を行う場合

キューやスタックなどのコンテナクラスを独自に実装する場合、内部的にCollectionや配列を持って要素を格納する方法が考えられます。可変長のCollectionは便利なのですが、性能を必要とするような場合は配列の方が好まれます。配列を使う場合、最初にある程度のサイズの配列を生成して、追加インデックス・取り出しインデックスを管理する方法が考えられます。要素を追加したり取り出す際には 追加インデックス・取り出しインデックスを更新していきます。そのようなクラスにおいて コンテナから要素を削除した場合に 追加インデックス・取り出しインデックスの更新だけを行ってしまうと、削除した要素への参照が配列に残ったままになってしまい、メモリリークの原因になります。

一般的に言うと、予めある程度のメモリ領域を確保して その領域内の各要素に対して使用中・未使用の管理を行う場合は メモリリークを起こす可能性があるため注意が必要です。このような場合には 要素が未使用になった場合は、その要素が保持していたオブジェクト参照をクリアする必要があります。具体的にはnullを設定します。

キャッシュの仕組みを実装する場合

Map等を利用してメモリキャッシュを実装する場合、不要になったエントリは定期的に削除する必要があります。不要になったエントリの削除を忘れるとメモリリークの原因になります。

不要になった(参照されなくなった)エントリを削除するにはjava.lang.refパッケージのWeakReferenceとSoftReferenceを利用することができます。いずれもコンストラクタでReferenceQueueのインスタンスを渡すと、不要になったタイミングを検出することができます。WeakReferenceを使うと 不要になったタイミングを検出することができ、SoftReferenceを使うと 不要になってから一定時間経過したタイミングを検出することができます。

また、標準ライブラリにWeakReferenceを利用した便利なクラスがいくつか用意されています。1つはjava.util.WeakHashMapです。キーが不要になると そのキーと値のエントリを自動で削除してくれます。もう1つはjava.lang.ThreadLocalです。スレッドごとにスレッドローカルな変数を提供してくれるクラスですが、不要なエントリを自動で削除してくれます(詳細は「マルチスレッド」の章の「ThreadLocalの実装」を参照してください)。

リスナやコールバックを登録する場合

リスナやコールバックを登録する場合、不要になったら削除をしないとメモリリークの原因になります。

このような場合にも 不要になったら自動的に削除されるようにWeakReferenceやWeakHashMapを利用することができます。

オーバーライド可能なObjectのメソッド

Objectは全てのクラスやインタフェースのスーパークラスです。いくつかの基本的なメソッドを実装していますが、サブクラスでオーバーライド可能なメソッドは次の通りです。

  • public  boolean  equals(Object  obj)
  • public  int  hashCode()
  • public  String  toString()
  • protected  Object  clone()  throws  CloneNotSupportedException
  • protected  void  finalize()  throws  Throwable

これらのメソッドをサブクラスでオーバーライドする場合には、一般契約を守る必要があります。一般契約の内容はJavaDocに記載されていますが、toString()以外は どれも解説が必要なものばかりです。clone()とfinalize()はこれまでの章で取り上げましたので、この章では equals()とhashCode()を取り上げます。

奥が深いequals()メソッド

Object.equals() は演算子==と同様で オブジェクトが同一かどうか(同一性)の判定を行います。データを表すクラスなどでは equals()をオーバーライドして、オブジェクトが表現する情報が等価かどうか(同値関係を返すようにすることができます。

equals()をオーバーライドする際には、JavaDocに記載されている一般契約に従う必要があります。equals()は必ずオーバーライドしないといけないわけでもなく、オーバーライドしない方が良い場合もあります。ここでは equals()のオーバーライドが必要なケース・不要なケースと、equals()の一般契約について見ていきます。

equals()のオーバーライドが必要なケース、不要なケース

オブジェクトの同一性のほかに、オブジェクトが表す情報が同じかどうかを表現したい場合にequals()メソッドをオーバーライドします。これは主にデータを表すようなクラスが該当します。逆に次の条件に当てはまるような場合は オーバーライドする必要はありません

  • 論理的に等価かどうかの判定が必要でない場合。データを表すクラスとは反対に 動作が中心となるようなクラス(Thread等)が該当します。
  • スーパークラスがequals()をオーバーライドしていて、サブクラスでもスーパークラスのequals()の処理内容で十分な場合
  • クラスがprivateやパッケージprivateで equals()メソッドが呼ばれないことが明らかな場合。ただし、標準ライブラリのメソッドから 間接的にequals()が呼び出される場合もあるので注意が必要です。例えばCollectionの要素として格納すると、Collectionのcontains()やremove()を呼び出すと 間接的に要素のequals()が呼び出されます。

equals()が返す同値関係

equals()は自身のオブジェクトを他のオブジェクトと比べた場合の同値関係を返します。異なるオブジェクトであっても オブジェクオが表すデータや情報が同一であれば 同値関係をtrueとすることができます。これはオブジェクトが表すデータや情報が等価かどうかを判定するために便利な仕組みではありますが、同値関係をどのように定義するかについては注意が必要です。と言うのも、equals()は コンテナクラスにおいて要素を識別する用途にも使われます。例えばHashSetの場合、contains()、add()、remove()などで 同一の要素か判定するためにequals()を利用しています。そのため、同値関係の定義が適切でないと 例えばremove()で意図しない要素を削除してしまいかねません。

オブジェクトが等しいときだけequals()がtrueを返す場合は、演算子==と同様で 最も厳しい同値条件になります。equals()をオーバーライドする場合は、オブジェクトのクラスやフィールドの値、メソッドの戻り値などを組み合わせて同値条件を定義することになりますが、コンテナクラスの要素とした場合に 他のオブジェクトと識別がつくようにする必要があります。

また、同じクラスのオブジェクト同士の同値関係は 比較的明確に定義できる場合がほとんどだと思いますが、equals()の引数はObjectであるため 異なるクラスのオブジェクトとの同値関係を定義する必要があります。比較対象のオブジェクトと継承関係がない場合は特に問題ないのですが、継承関係がある場合に問題が出てきます。比較対象のオブジェクトと継承関係があると 部分的に一致する場合が出てきます。そして、部分一致を同値としたいケースもあれば同値としたくないケースもあり、どちらかに倒さないといけないのですが、どちらに倒しても誤用を招く可能性を残してしまいます。これについては後ほど説明しますが、継承が絡むと同値関係の定義が単純ではなくなるということです。

equals()メソッドの一般契約

equals()は次の契約を守る必要があります。

  1. 反射性:xがnullでない場合に x.equals(x)はtrueを返す。
  2. 対象性:x、yがnullでない場合に y.equals(x)がtrueを返す場合に限り x.equals(y)がtrueを返す。
  3. 推移性:x、y、zがnullでない場合に x.equals(y)がtrueで y.equals(z)がtrueならば、x.equals(z)はtrueを返す。
  4. 一貫性:x、yがnullでない場合に x.equals(y)を何度呼び出しても、equals()による比較に使われた情報が変更されていなければ 一貫してtrueを返すか、一貫してfalseを返す。
  5. xがnullでない場合に x.equals(null)はfalseを返す。

1については 故意に違反しようとしない限り違反するケースは無いと思います。5については 後ほど説明する equals()のガイドラインに従えば自然と守られます。2、3、4については意図せず違反してしまう場合もあるので、それぞれの違反例を見てみます。

対象性に違反する例

あるデータを 丸めたり変換したりフィルタリングした情報を保持するクラスを定義するような場合に 対象性に違反するケースが出てきます。int値の絶対値を保持するクラスAbsoluteIntegerを例に挙げます。

class AbsoluteInteger {
    private final int i; // 絶対値
    
    public AbsoluteInteger(int i) {
        this.i = Math.abs(i);
    }
    
    @Override
    public boolean equals(Object obj) {
        // 同じクラスならフィールドを比較
        if(obj instanceof AbsoluteInteger) {
            return this.i == ((AbsoluteInteger) obj).i;
        }
        // 便利なようにIntegerとも比較できるようにする
        if(obj instanceof Integer) {
            return this.i == Math.abs((Integer) obj);
        }
        return false;
    }
    
    @Override
    public String toString() {
        return getClass().getSimpleName() + "[" + i + "]";
    }
}

これは次のように対象性に違反しています。

AbsoluteInteger absi = new AbsoluteInteger(-123);
Integer i = 123;

System.out.println("absi.equals(i):" + absi.equals(i)); // true
System.out.println("i.equals(absi):" + i.equals(absi)); // false

対象性に違反していると、次のようにコンテナクラスの要素にした場合に、remove()で削除しようとすると 意図しない要素が削除されてしまいます。

AbsoluteInteger absi = new AbsoluteInteger(-123);
Integer i = 123;

List<Integer> iList = new ArrayList<>();
iList.add(i);
System.out.println(iList);  // [123]
iList.remove(absi);
System.out.println(iList);  // []

AbsoluteInteger.equals()では 利便性のためにIntegerオブジェクトと比較できるようにしました。しかし、コンテナクラスの要素にした場合は 同一の要素か判定するために間接的にequals()が呼び出され、意図しない要素が削除されてしまいかねません。

継承関係のないクラスのオブジェクトと比較する場合は 基本的には同値関係はfalseにします。上の例のような利便性を提供したい場合は、後述するpublicなビューメソッドで絶対値を返すメソッドを提供するようにします。

推移性に違反する例

具象化クラスとそのサブクラスのオブジェクトを混在させて扱うような場合に 推移性に違反するケースが出てきます。用紙のサイズを表すPaperクラスと、そのサブクラスのColorPaperクラスを例に見てみます。

class Paper {
    private final int width;    // 単位:ミリ
    private final int height;    // 単位:ミリ
    
    Paper(int width, int height){
        this.width = width;
        this.height = height;
    }
    
    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof Paper)) {
            return false;
        }
        Paper other = (Paper) obj;
        return ((this.width == other.width) && (this.height == other.height));
    }

    // hashCode()も適切にオーバーライド。
}

class ColorPaper extends Paper {
    private final Color color;
    
    ColorPaper(int width, int height, Color color){
        super(width, height);
        this.color = color;
    }
}

ColorPaperクラスでは色の要素をフィールドとして追加しているため、equals()をオーバーライドする必要があります。ColorPaperのequals()を次のように定義したとします。

// 対象性に違反している例
    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof ColorPaper)) {
            return false;
        }
        ColorPaper other = (ColorPaper) obj;
        return super.equals(obj) && this.color == other.color;
    }

これは PaperとColorPaperを比較した場合に 対象性に違反してしまいます。

Paper a4 = new Paper(210, 297);
ColorPaper a4red = new ColorPaper(210, 297, Color.red);

// 対象性違反
System.out.println("a4.equals(a4red):" + a4.equals(a4red));    // true
System.out.println("a4red.equals(a4):" + a4red.equals(a4));    // false

それでは、対象性に違反しないように ColorPaperのequals()を次のようにしてみます。

// 推移性に違反している例
    @Override
    public boolean equals(Object obj) {
        if(obj instanceof ColorPaper) {
            ColorPaper other = (ColorPaper) obj;
            return super.equals(obj) && this.color == other.color;
        }
        if(obj instanceof Paper) {
            return super.equals(obj);
        }
        return false;
    }

しかし、今度は推移性に違反してしまいます。

Paper a4 = new Paper(210, 297);
ColorPaper a4red = new ColorPaper(210, 297, Color.red);
ColorPaper a4blue = new ColorPaper(210, 297, Color.blue);

// 推移性違反の例
System.out.println("a4.equals(a4red):" + a4.equals(a4red));          // true
System.out.println("a4.equals(a4blue):" + a4.equals(a4blue));        // true
System.out.println("a4red.equals(a4blue):" + a4red.equals(a4blue));  // false

また、ColorPaper以外のPaperのサブクラスのオブジェクトと比較した場合も、widthとheightさえ一致すればequals()の結果はtrueになってしまい 同値関係として適切ではありません。

スーパークラスではサブクラスでどのようなフィールドを追加されるかは知り得ることはできないため、スーパークラスのオブジェクトから見ると サブクラスで追加したフィールドだけが異なるオブジェクトは全て等価に見えてしまいます。この推移性違反を直すためには、サブクラスであるColorPaperのequals()の変更では対処できず、スーパークラスであるPaperのequals()を変更する必要があります。一つ考えられる案は サブクラスも含めて自身のクラスのオブジェクト以外の比較をfalseとする案です。Paperのequals()を次のように変更します。

    // 自身のクラス以外は比較対象にしない。
    @Override
    public boolean equals(Object obj) {
        if(getClass() != obj.getClass()) {
            return false;
        }
        Paper other = (Paper) obj;
        return ((this.width == other.width) && (this.height == other.height));
    }

これであれば、対象性にも推移性にも違反しません。しかし、この案は 時にサブタイピングの恩恵を捨てることもあります。例えばPaperクラスが 用紙サイズに対応しているかどうかを返す 次のようなクラスメソッドを提供するとします。

    private static Set<Paper> supportedSizeSet = Set.of(
            new Paper(420, 594),    // A2サイズ
            new Paper(297, 420),    // A3サイズ
            new Paper(210, 297));   // A4サイズ
    
    public static boolean isSupportedSize(Paper paper) {
        return supportedSizeSet.contains(paper);
    }

引数にサブクラスのオブジェクトを渡した場合は、期待する答えが返ってこなくなります。

Paper a4 = new Paper(210, 297);
ColorPaper a4red = new ColorPaper(210, 297, Color.red);

System.out.println(Paper.isSupportedSize(a4));     // true
System.out.println(Paper.isSupportedSize(a4red));  // false

Effective Javaでは、上のような例を挙げて 具象化クラスとそのサブクラスのオブジェクトを混在させた場合に equals()の一般契約を守りつつ リスコフの置換原則を保つ方法は無いと結論づけています。しかし、上の例のようなケースの問題の本質は違うところにあると考えます。

isSupportedSize()は、引数の要素のサイズをサポートしているかどうかを返すメソッドです。本来ならば supportedSizeSetの要素を走査して、引数の要素のwidth・heightと比較する処理を行うところですが、たまたまPaperクラスにはwidth・heightの2つのフィールドしかなく Paper.equals()でその2つのフィールドの比較を行っているため、equals()で代替出来ているだけに過ぎません。そして、equals()で代替できるため、ループ処理を行わなくても contains()を呼び出すだけでメソッドの機能を実現できているだけに過ぎません

Paperクラスに他のフィールドがある場合はequals()で代替することはできませんし、サブクラス化されることが明確である場合もequals()で代替すべきではないと思います。そのような場合は equals()の代わりにwidth・heightを比較するメソッドを用意してループ処理で比較をするべきです。また、java.awt.Pointなどの既存のクラスを利用するためメソッドが追加できないような場合は、コードが長くはなりますが ループ処理で ゲッターを通じてwidth・heightを取り出して比較を行うべきです。サブタイピングの恩恵を受けたいときは、果たしてそれがequals()で比較するべきなのかどうかは一考してみる必要があります

整理すると、equals()は同値関係を返すメソッドですが、具象化クラスとそのサブクラスのオブジェクトを同値とするのかどうかが一つのポイントになります。コンテナクラスに要素として格納することを考えると、remove()で意図しない要素を削除しないよう 同値としない方が適切かも知れません。一方で、サブタイピングの恩恵を受けるため 同値としたい場合もあるかも知れません。しかし その場合は equals()が適切なのかどうかは考えてみる必要があります。

推移性に違反せずに 具象化クラスに属性を追加する方法

上で示した通り、具象化クラスとそのサブクラスのオブジェクトを混在させる場合 推移性の問題を考慮する必要があります。推移性の問題に煩わされずに 具象化クラスに属性を追加する別の方法があります。それは継承ではなくコンポジションを用いる方法です。上のColorPaperを継承ではなくコンポジションで実現すると次のようになります。

class CompColorPaper {
    private final Paper paper;
    private final Color color;
    
    public CompColorPaper(int width, int height, Color color) {
        this.paper = new Paper(width, height);
        this.color = color;
    }
    
    // Paperとして扱う場合のビューを返す。
    // 場合によってはディフェンシブコピーを行う。
    public Paper asPaper() {
        return paper;
    }
    
    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof CompColorPaper)) {
            return false;
        }
        CompColorPaper other = (CompColorPaper) obj;
        return paper.equals(other.paper) && color.equals(other.color);
    }
}

コンポジションにすると推移性違反の問題に悩まされることはなくなります。この方法の場合も サブタイピングの恩恵を捨てることにはなりますが代わりにPaperクラスとして扱うためのasPaper()というpublicなビューメソッドを提供することで 利便性を補っています

一貫性に違反する例

equals()の判定が 外部の情報に依存するような場合に 一貫性に違反するケースが出てきます。例えばjava.net.URLクラスでは equals()メソッドでホストの比較を行いますが、InetAddress.getByName()を用いてホスト名からIPアドレスの解決を行い IPアドレス同士の比較を行います。名前解決のルールによっては ネットワークにアクセスすることになり、ネットワークに障害が発生している場合はequals()の判定が正しく行われません。また、DNSラウンドロビンで複数IPアドレスを割り当てている場合などは、同じホスト名でも異なるIPアドレスが返ってくる可能性があります。

外部の情報に依存せず、 equals()による比較に使われた情報が変更されていなければ、一貫してtrueを返すか、一貫してfalseを返すようにしなくてはいけません。

equals()のガイドライン

equals()を実装する場合には 概ねのガイドラインがあります。ガイドラインは次の通りです。

  1. ==演算子 で自身と引数のオブジェクトが同一オブジェクトかどうか判定します
    同一オブジェクトであればtrueを返します。
  2. 引数のオブジェクトの型検査を行います。一般的にはinstanceofで自身またはそのサブクラスの型であるかどうかを判定します。しかし、推移性違反の例で述べた通り、具象化クラスとそのサブクラスを混在させて比較する必要があり、かつ具象化クラスとそのサブクラスを同値としない場合は、getClass()で自身と引数のオブジェクトが同一クラスかどうかを判定します。
    型が不一致であればfalseを返します。
    尚、instanceofで型検査を行う場合は 引数がnullのときfalseが返るのでnullチェックも兼ねます。getClass()で型検査を行う場合は nullチェックが必要です。
  3. オブジェクトを識別するような主要なフィールドに対して、自身のフィールドと引数のオブジェクトのフィールドをそれぞれ比較します。注意点は次の通りです。
    • double、float以外のプリミティブ型の場合は ==演算子で比較します。
      double、floatの場合はNaN、NEGATIVE_INFINITY、POSITIVE_INFINITYという特別な値があるため、Double.compare()やFloat.compare()メソッドで比較を行います
    • オブジェクト参照の場合は再帰的にequals()を呼び出します。nullが許容される場合は Objects.equals() 等を利用すると nullを含む同値チェックを行ってくれるので便利です。
    • フィールドを比較する順序は オブジェクト同士で差異が出易いフィールドから比較していくのが効率的です。

equals()の注意点

equals()における注意点は次のようになります。

  • equals()をオーバーライドする場合は、必ずhashCode()もオーバーライドしますequals()がtrueを返すオブジェクト同士は hashCode()が同じ値を返す必要があります。そうしないと、例えばHashSetやHashMap等のハッシュバケットに基づくコンテナに格納した場合に 同値の要素を検索することができなくなってしまいます。
  • equals()の引数はObject型です。引数の型を自身のクラス等のObjectではない型にしてしまうと、オーバーライドではなくオーバーロードになってしまいます。この誤りを防ぐためには IDEによる@Overrideアノテーションの自動付与が有効です
  • equals()に利便性や柔軟性を求めすぎると 対象性・推移性に違反してしまいがちです。利便性や柔軟性が必要な場合は、ビューメソッド等の別のメソッドとして定義すべきかどうか検討してみると良いです。

hashCode()メソッド

Object.equals()をオーバーライドしているクラスでは、Object.hashCode()も同様にオーバーライドする必要があります。hashCode()もequals()と同様、一般契約がJavaDocにまとめられています。

hashCode()の一般契約

hashCode()の一般契約は次の通りです。

  1. hashCode()を何度も呼び出した場合、equals()の比較で使われる状態が変わらない限り 同じ値を返す必要があります。ただし、アプリケーションを再起動した場合、再起動前と再起動後で異なる値を返すことは構いません。
  2. equals()がtrueを返すオブジェクト同士では、hashCode()が同じ値を返す必要があります
  3. 一方で equals()がfalseを返すオブジェクト同士では、hashCode()は違う値を返す必要はありません

2番目が最も重要な項目で、equals()メソッドのところでも触れましたが、これに違反するとHashSetやHashMap等のハッシュバケットに基づくコンテナに格納した場合に同値の要素を検索することができなくなってしまいます。正確には、ハッシュコードが異なっていても たまたま同じハッシュバケットに割り当てられるケースもあるため、アプリケーションを再起動するたびに 同値の要素を発見できたりできなかったりと非決定的な動作になってしまいます。

hashCode()の戻り値を定数にすると、1~3の項目を全て満たすことになりますが、推奨されません。HashSetやHashMap等のコンテナに格納する場合、そのクラスの全てのオブジェクトが同じハッシュバケットに格納されてしまい 効率が悪くなってしまうからです。そのため、hashCode()の戻り値をオブジェクトに依らず定数にしてしまうことは 一般契約には違反しないのですが推奨されません。

hashCode()のガイドライン

hashCode()もequals()と同様、概ねのガイドラインがあります。ガイドラインは次の通りです。

  1. 戻り値用の変数を用意します。ここではresultとします。
  2. equals()の比較で使われる 最初のフィールドのハッシュコードを算出して、resultの初期化にします。ハッシュコードの算出は次の通り フィールドの型と値により分岐します。
    1. フィールドがプリミティブ型の場合、ラッパークラスのhashCode()クラスメソッドを用いて算出します。例えばint型のフィールドであれば、Integer.hashCode()を用います。
    2. フィールドがオブジェクト参照の場合、オブジェクト参照のhashCode()を再帰的に呼び出します。nullの場合は一般的に0をハッシュコードとします。
    3. フィールドが配列の場合、Arrays.hashCode()を利用することもできます
  3. equals()の比較で使われる 続くフィールドに対して、2と同様にハッシュコードを計算し、result ×31に計算したハッシュコードを足して、新たなresultとします。31である必要はありませんが、奇数で素数の31が良く使われます
  4. 残りのフィールドに対しても3を繰り返し、最終的なresultを戻り値とします。

実装すると便利なインタフェース

Objectのメソッドではないのですが、equals()と似た 基本的な機能を定義しているComprableインタフェースを取り上げます。Comparableを実装していると順序付けに基づいた各種機能を簡単に利用することができます

Comparableインタフェース

Comparableインタフェースを実装すると、オブジェクトの自然順序付けが定義されます。自然順序付けが定義されていると、標準ライブラリを利用して ソートができたり、最大値・最小値を求めることができるようになります。具体的には次のような機能を利用することができます。

自然順序付けが定義されていると利用できる主な機能
クラスメソッド概要
java.util.Collectionssort引数のコレクションの要素を自然順序付けに従ってソートします。
max引数のコレクションから自然順序付けで最大の要素を返します。
min引数のコレクションから自然順序付けで最小の要素を返します。
binarySearch引数のコレクションから バイナリ・サーチ・アルゴリズムに基づいて 指定された要素を検索します。
java.util.Arrayssort引数の配列の要素を自然順序付けに従ってソートします。
java.util.TreeSet要素が自然順序付けに従ってソートされているSetの実装です。
java.util.TreeMapキーが自然順序付けに従ってソートされているMapの実装です。

コレクションや配列の要素がComparableインタフェースを実装していなくても、別途Comparatorを指定することによって 上の表の機能を利用することもできます。しかし、頻繁に上の表のような機能を利用する場合は Comparableを実装しておいた方が便利です。

Comparable.compareTo()の一般契約

Comparableインタフェースの唯一の抽象メソッドは compareTo()です。Object.equals()と似ていますが、equals()の同値比較に加えて 順序付けを定義する必要があります

compareTo()では、自身が引数で渡されたオブジェクトより小さい場合は負の数、等しい場合は0、大きい場合は正の数を返します。Comparable.compareTo()の一般契約は次の通りです。対象性や推移性など、equals()の一般契約に似ています。尚、sgn()は 負の数を与えると-1、0を与えると0、正の数を与えると1を返す関数です。

  1. 対象性:全てのxとyに関して sgn(x.compareTo(y)) == -sgn(y.compareTo(x))が保証される必要があります。これは、y.compareTo(x)が例外を投げる場合は x.compareTo(y)も例外を投げることを意味します。
  2. 推移性:全てのzに関して (x.compareTo(y) > 0 && y.compareTo(z) > 0) の場合、x.compareTo(z) > 0 でなければなりません。また、x.compareTo(y) == 0 の場合、sgn(x.compareTo(z)) == sgn(y.compareTo(z)) でなければなりません。
  3. equals()との関連:(x.compareTo(y) == 0) の場合 x.equals(y) であることが強く推奨されますが 必須ではありません。しかし、この条件を守っていない場合は 明確にドキュメント化されるべきです。

1番目と2番目の項目で 対象性・推移性を守る必要がありますが、Object.equals()と同様、具象化クラスとそのサブクラスを混在させる場合に 推移性違反の問題が出てきます。その場合は Object.equals()の場合と同様の対処方法を採ることができます。

equals()とcompareTo()の同値判定が異なる場合、格納するコンテナクラスの種類によって 同一要素と扱われるのかそうでないのかが異なってきます。ArrayList、HashSet、HashMapといったクラスはequals()に基づいた同一要素判定を行い、TreeSet、TreeMapといったクラスはcompareTo()に基づいた同一要素判定を行います。そのため、equals()はtrueを返すがcompareTo()が0を返さない2つのオブジェクトは、HashSet等では同一要素として扱われますが、TreeSet等では異なる要素として扱われます。反対に compareTo()が0を返すがequals()はfalseを返す2つのオブジェクトは、TreeSet等では同一要素として扱われますが、HashSet等では異なる要素として扱われます(BigDecimalが後者のクラスの1つです)。

これらの一般契約に従わない場合、冒頭の表で示した Collections.sort()、Arrays.sort()といった 自然順序付けに基づく機能が正しく動作しなくなるため 注意が必要です。

compareTo()メソッドのガイドライン

compareTo()メソッドを実装することは Object.equals()メソッドを実装することに似ていますが、次の点が大きく異なります。

  • Comparableは総称型であるため、Object.equals()とは異なり 引数はT型になります。Object.equals()と違って 異なるクラスのオブジェクトとの自然順序を定義付ける必要はなく、引数が異なるクラスのオブジェクトの場合 ClassCastExceptionを投げて構いません。(型チェックをしなくても構わないということです。)
  • 引数がnullの場合 NullPointerExceptionを投げて構いません。(nullチェックをしなくても構わないということです。)

compareTo()メソッドのガイドラインは次のようになります。

  1. オブジェクトを識別するような主要なフィールドに対して、自身のフィールドと引数のオブジェクトのフィールドをそれぞれ比較します。注意点は次の通りです。
    • フィールドがプリミティブ型の場合はラッパークラスのcompare()クラスメソッドで比較を行います。ときどき 自身のフィールドの値から引数のフィールドの値を引いて 差を戻り値とする実装を見かけますが、オーバーフローが発生する場合浮動小数点の誤差が発生する場合に正しくない結果になるため、ラッパークラスのcompare()を使うようにします。
    • フィールドがオブジェクト参照の場合は再帰的にcompareTo()を呼び出します。参照先のオブジェクトがComparableを実装していなかったり、自然順序付け以外の順序付けを採用したい場合は、定義済みのComparatorや独自のComparatorを使用することができます。定義済みのComparatorとしてはString.CASE_INSENSITIVE_ORDERなどがあります。
    • フィールドを比較する順序は オブジェクト同士で差異が出易いフィールドから比較していくのが効率的です。

intのフィールドfirst、second、thirdを持つクラスSomeClassのcompareTo()メソッドは次のようになります。

class SomeClass implements Comparable<SomeClass> {

    private int first;
    private int second;
    private int third;

    public int compareTo(SomeClass other) {
        int result = Integer.compare(first, other.first);
        if(result != 0) {
            return result;
        }
        result = Integer.compare(second, other.second);
        if(result != 0) {
            return result;
        }
        return Integer.compare(third, other.third);
    }
}

Java SE8からComparatorクラスにコンパレータ構築メソッドと呼ばれるクラスメソッドの集合が追加されました。コンパレータ構築メソッドを使うと、流れるようにcompareTo()メソッドを記述することができます。

コンパレータ構築メソッド

始めにComparatorクラスのクラスメソッドcomparingXXX()を呼び出して 最初のフィールドを比較するComparatorインスタンスを取得します。続いて、取得したComparatorインスタンスに対して インスタンスメソッドthenComparingXXX()を呼び出して 次のフィールドを比較するComparatorインスタンスを取得します。比較したいフィールドの数だけインスタンスメソッドの呼び出しを繰り返して、コンパレータを構築します。

メソッドの引数にはオブジェクトから比較対象のフィールドを抽出する関数(キー抽出子)をラムダ式やメソッド参照で指定します。メソッドによってはComparatorを引数に取るものもあります。

構築したコンパレータに対して、比較したい2つのオブジェクトを渡してcompare()メソッドを呼び出すことによって順序比較を行うことができます。

Comparatorクラスの主なコンパレータ構築メソッドは次の通りです。

Comparatorクラスの主なコンパレータ構築メソッド
メソッド引数比較内容
staticcomparingIntキー抽出子抽出したint値を比較します。
staticcomparingLongキー抽出子抽出したlong値を比較します。
staticcomparingDoubleキー抽出子抽出したdouble値を比較します。
staticcompareキー抽出子抽出したオブジェクトを自然順序付けに従って比較します。
staticcompareキー抽出子、
コンパレータ
抽出したオブジェクトをコンパレータに従って比較します。
thenComparingIntキー抽出子抽出したint値を比較します。
thenComparingLongキー抽出子抽出したlong値を比較します。
thenComparingDoubleキー抽出子抽出したdouble値を比較します。
thenComparingコンパレータ要素自体をコンパレータに従って比較します。
thenComparingキー抽出子抽出したオブジェクトを自然順序付けに従って比較します。
thenComparingキー抽出子、
コンパレータ
抽出したオブジェクトをコンパレータに従って比較します。

先ほどのSomeClassのcompareTo()を コンパレータ構築メソッドを使うと次のように書き換えることができます。コンパレータ構築メソッドを使うと 何を比較しているのかが一目で識別できるようになります

class SomeClass implements Comparable<SomeClass> {
    
    private static final Comparator<SomeClass> COMPARATOR =
            Comparator.comparingInt((SomeClass e) -> e.first)
                .thenComparingInt(e -> e.second)
                .thenComparingInt(e -> e.third);
    
    private int first;
    private int second;
    private int third;

    @Override
    public int compareTo(SomeClass o) {
        return COMPARATOR.compare(this, o);
    }
    
}