このドキュメントは10年位前に新人エンジニア向けに作成した資料の転載になります。
一部APIや構文など古臭い箇所が目に付く場合もありますが、考え方については一度覚えたら長く使える基礎の部分を整理したつもりなので、今の若いエンジニアの方にも参考になればと思います。
理解のポイント
- Javaにおける等価演算子"=="とequalsメソッドの違いを理解する
- Javaにおけるequalsメソッドの実装方法を理解する
- JavaにおけるhashCodeメソッドの意味を理解する
前提条件
以下のような、Userクラスが存在するとして、読み進めてください。 Userクラスは内部状態として、IDと名前を保持します。
class User {
/** ID */
private int id;
/** 名前 */
private String name;
/**
* コンストラクタです。
*
* @param id ID
* @param name 名前
*/
public User(int id, String name) {
this.id = id;
this.name = name;
}
// 以下、省略
}
同一性と同値性の違い
オブジェクトの”同一性”とは?
User user1 = new User(1, "田中");
User user2 = user1;
user1とuser2は同じオブジェクトの参照を保持しています。 従って、user1とuser2が参照しているオブジェクトは"同一"です。
オブジェクトの”同値性(等価性)”とは?
User user3 = new User(1, "田中");
User user4 = new User(2, "鈴木");
User user5 = new User(1, "鈴木");
user3とuser5はIDが等しい。 user4とuser5は名前が等しい。
従って、user3とuser5はIDの値において"同値"のオブジェクトです。 user4とuser5は名前の値において"同値"のオブジェクトです。
オブジェクトの同値性はプログラマ自身がビジネスのルールに従い実装する必要があります。
オブジェクトの同値性は、該当するクラスのequalsメソッドに定義します。
もし、UserクラスのオブジェクトをIDの値において同値性を判定するルールがあるならば、equalsメソッドに、IDの値を比較する処理を記述します。
もし、Userクラスのオブジェクトを名前の値において同値性を判定するルールがあるならば、equalsメソッドに、名前の値を比較する処理を記述します。
オブジェクトの同一性の判定方法
オブジェクトの同一性は比較演算子"=="で判定します。 上記user1とuser2は同一のオブジェクトなので
user1 == user2 // ⇒ true
の結果は"true"となります。
user3、user4、user5は全て異なるオブジェクトなので
user3 == user4 // ⇒ false
user3 == user5 // ⇒ false
user4 == user5 // ⇒ false
の結果は、全て"false"となります。
オブジェクトの同値性の判定方法
オブジェクトの同値性はそれぞれのクラスに実装されている"equals"メソッドで比較します。
UserクラスのequalsメソッドがIDを比較条件として実装されている場合、 equalsメソッドの結果は以下の通りとなります。
user3.equals(user4) // ⇒ false
user3.equals(user5) // ⇒ true
user4.equals(user5) // ⇒ false
Userクラスのequalsメソッドが名前を比較条件として実装されている場合、equalsメソッドの結果は以下の通りとなります。
user3.equals(user4) // ⇒ false
user3.equals(user5) // ⇒ false
user4.equals(user5) // ⇒ true
equalsメソッドの実装
ユーザIDによるequalsメソッドの実装例
Userクラスのオブジェクト同値性をIDによって判定する場合は、Userクラスに以下のようにequalsメソッドを 実装します。
/**
* このクラスのインスタンスと引数で渡されたオブジェクトが
* 同値であるばあいtrueを返します。
* 引数で渡されたオブジェクトがUserクラスのインスタンスであり、
* idの値が等しい場合、同値であるとみなされます。
*
* @return 引数で渡されたオブジェクトがUserクラスのインスタンスであり、idが等しい場合true。
*/
public boolean equals(Object other) {
if (this == other) { // 引数で渡されたオブジェクトがこのオブジェクト自身であった場合true
return true;
}
if (!(other instanceof User)) { // 引数で渡されたオブジェクトが、Userクラスのオブジェクト
return false; // では無い場合はfalse。
}
User otherUser = (User) other;
if (this.id == otherUser.getId()) { // IDの値を比較し、等しければtrue、等しくなければfalse。
return true;
}
return false;
}
ユーザ名(name)によるequalsメソッドの実装例
Userクラスのオブジェクト同値性を名前によって判定する場合は、Userクラスに以下のようにequalsメソッドを 実装します。
値の比較がidからnameに代わるだけで、残りの処理は同じです。
/**
* このクラスのインスタンスと引数で渡されたオブジェクトが
* 同値であるばあいtrueを返します。
* 引数で渡されたオブジェクトがUserクラスのインスタンスであり、
* nameの値が等しい場合、同値であるとみなされます。
*
* @return 引数で渡されたオブジェクトがUserクラスのインスタンスであり、nameが等しい場合true。
*/
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof User)) {
return false;
}
User otherUser = (User) other;
if (this.name.equals(otherUser.getName())) {
return true;
}
return false;
}
hashCodeメソッドを理解する
hashCodeとは
equalsメソッドを実装した場合は、hashCodeメソッドも実装する必要があります。 なお、hashCodeメソッドは以下のルールに従って実装します。
- equalsメソッドの結果がtrueとなるオブジェクトは、hashCodeメソッド呼び出しの結果同じ値を返す必要がある。 (equalsがfalseとなるオブジェクトが同じhashCodeの結果を返すことは、かまわない)
Userクラスにおいて、IDの値で同値性を判定するequalsメソッドを実装した場合、hashCode メソッドは、一つの例としては、下記のように実装します。
例1
/**
* ハッシュコードを返します。
*
* @return このクラスのインスタンスのハッシュ値
*/
public int hashCode() {
return this.id;
}
equalsメソッドでは、IDの値を元に同値性を判定しているので、IDの値が等しい、つまり、 equalsメソッドがtrueを返すオブジェクトのhashCodeメソッドは、上記の実装により同じ値を返すことになり、 上記hashCodeメソッドの処理は上記のルールに対して妥当であると言えます。
例2
/**
* ハッシュコードを返します。
*
* @return このクラスのインスタンスのハッシュ値
*/
public int hashCode() {
return 0;
}
上記hashCodeメソッドの処理も、実は妥当であるといえます。 全てのオブジェクトのhashCodeは0を返す。つまり、equalsメソッドがtrueになるオブジェクトは 全てhashCodeの戻り値として、同じ値(0)を返します。 equalsメソッドがtrueにならない(equalsメソッドの呼び出しの結果がfalseとなる)オブジェクトが 同じhashCodeメソッドを返すことは問題ないので、 上記実装もまた、hashCode実装のルールに照らした場合問題ないとみなされます。
ただし、例2の実装は、推奨できるものではありません。 hashCodeを実装する場合は、少なくとも例1のように実装して下さい。
hashCodeを正しく実装しなかった場合
ハッシュアルゴリズムを使用するクラス(HashMap、HashSet等)のインスタンスに、hashCodeを正しく実装しないオブジェクト追加した場合、期待した動作を得られません。
ハッシュアルゴリズムとは
オブジェクトの格納、検索を行う際に、あらかじめハッシュ値を計算し、得られたハッシュ値に 基づいてオブジェクトの格納、検索を行うことで、処理を効率化するアルゴリズムです。
ハッシュアルゴリズムを理解するには、ArrayListとHashSetの動作の違いを比較するとよいでしょう。
ArrayListとHashSetはともにCollectionインターフェースを実装するクラスですが、以下の ような動作の違いがあります。
(HashSetはハッシュアルゴリズムを実装している。)
ArrayListの場合
ArrayListは追加された要素を1列のリストに格納して保持します。 格納されている要素を取り出すときは、リストの先頭から順番にオブジェクトのequalsメソッド を呼び出して、equalsメソッド呼び出しの結果がtrueとなる要素を戻り値として返します。
⇒ リストの要素の数が膨大で、かつ取り出したい要素がリストの後方に存在した場合、 検索効率が極端に悪化する可能性がある。
HashSetの場合
HashSetは追加された要素を以下の手順に従って格納、検索します。
- 追加対象のオブジェクトのhashCodeメソッドを呼び出します。
- hashCodeメソッドの戻り値ごとにオブジェクトを「部屋」(HashSetクラスの実装上の 用語はBucket)に分類し、hashCodeの部屋番号が付けられた「部屋」に 対象オブジェクトを格納します。
- 要素を取り出す場合は、hashCodeの値を元に、そのhashCodeの値を 部屋番号としてもつ「部屋」にまず対象となるオブジェクトを探しにいきます。
- 手順3で探し当てた「部屋」に格納されているオブジェクトに対して順番にequalsメソッドを 呼び出し、equalsメソッド呼び出しの結果がtrueとなる要素を戻り値として返す。
あらかじめ要素をhashCodeに基づく「部屋」に分類して保持しているため、 オブジェクト同士を比較する際、限られた数のオブジェクトを比較すればよく、検索 効率が向上します。
hashCodeが正しく実装されていない場合、同値のオブジェクトであるにもかかわらず 異なる部屋に対象となるオブジェクトを探しに行ってしまうため、対象となるオブジェクト が見つからないという事態が発生する可能性があります。
hashCodeが0を返すように実装されている場合、実質部屋番号0の「部屋」に全ての オブジェクトが格納されることになり、アルゴリズムとしてはArrayListに要素を追加する 同じとなり、ハッシュアルゴリズムの利点が得られません。
equalsとhashCodeの正体
equalsメソッドとhashCodeメソッドを実装しなかった場合
equalsメソッドとhashCodeメソッドはもともとはObjectクラスに実装されているメソッドです。
Objectクラスはすべてのクラスのスーパークラスです。
したがって、equalsメソッドとhashCodeメソッド が実装されていないクラスのインスタンスに対してequalsメソッドやhashCodeメソッドを呼び出した場合は (他に継承しているクラスが無いならば)親クラスであるObjectクラスで定義されたequalsメソッドやhashCode メソッドが呼び出されます。
たとえばequalsメソッドが実装されていないクラスのインスタンスに対してequalsメソッドが呼び出された 場合のメソッドの呼び出し順序は以下のとおりとなります。
- equalsメソッドが呼び出される。
- 該当インスタンスのクラスにequalsメソッドが定義されていないため、親クラスに定義されていないかさかのぼって探索する。
- 親クラスをさかのぼっていき、いずれかのクラスでequalsメソッドが定義されている場合は、そのequalsメソッドが呼び出される。
- 親クラスをさかのぼっていき、いずれのクラスでもequalsメソッドが定義されていない場合は、最終的にObjectクラスの 定義にたどり着き、Objectクラスで定義されているequalsメソッドが呼び出される。
equalsメソッドとhashCodeメソッドを実装することの意味
「equalsメソッド、hashCodeメソッドを実装する」とは(他に継承するクラスが無い場合)
- 「Objectクラスで定義されているequalsメソッド、hashCodeメソッドを オーバーライドすることで該当クラスのequalsメソッド、hashCodeメソッドの振る舞いを変更する」
ということを意味します。
Objectクラスに定義されているデフォルトのequalsメソッドの振る舞い
Objectクラスに定義されているequalsメソッドは「オブジェクトの同一性」に基づいて「オブジェクトの同値性」を判定します。
したがって、Userクラスでequalsメソッドを実装しない場合(Objectクラスのequalsメソッドをオーバーライドしない場合)、 UserクラスのインスタンスはIDでも名前でもなく、インスタンスがまったく同じものである(同一である)場合にequalsメソッドが trueを返す、ということになります。
// Objectクラスのequalsメソッドの振る舞い
Object obj1 = new Object();
Object obj2 = new Object();
Object obj3 = obj1;
obj1 == obj2; // false
obj2 == obj3; // false
obj1 == obj3; // true
// "=="よる比較と結果は同じ
obj1.equals(obj2); // false
obj2.equals(obj3); // false
obj1.equals(obj3); // true
// equalsメソッドを実装しないUserクラスの振る舞い
// (Objectクラスで定義されているequalsメソッドが呼び出されるため、振る舞いは一緒)
User user1 = new User();
User user2 = new User();
User user3 = user1;
user1 == user2; // false
user2 == user3; // false
user1 == user3; // true
user1.equals(user2); // false
user2.equals(user3); // false
user1.equals(user3); // true