java equalsとは?java equalsを通じてインスタンスが「同じ」かチェックしよう
JavaのObjectクラスにあるメソッドequalsは、何かのインスタンス同士が「同じ」かを調べるメソッドです。
ここで言う「同じ」について、意味として同じか、実体として同じかの区別がJavaではされるのです。そして、Javaではその違いが実に重要です。その違いを意識できているかで、Javaのプログラムが正しく動くかどうかが決まってしまうほどのものなのです。
この時点で、私が何を言わんとしているのか正直よくわからない、そういうことを考えたこともなかったからちょっと不安だな…という方もご安心ください。
この記事では、そもそも同じとは何ぞやというところからスタートして、Objet.equalsの具体的な例とここは押さえておきたい!!というポイントまで、分かりやすくお伝えします。
※この記事のサンプルは、Java 11の環境で動作確認しています
目次
1.equalsはインスタンスの「意味」が同じか調べるもの
1-1.「同じ」であるとはどういうことか
「同じ」であるとは、はたしてどういうことか。なかなか哲学じみた問いかけですが、あらためて考えてみるとちょっと面倒なものです。
例えば、壊れて修理に出したスマホが戻ってきたとします。スマホそのものは交換されて新品になりました。でも、古いスマホの中にあった写真やテキスト、ユーザ情報やデータは、サポートが新しいスマホに全部そのままコピーしてくれました。ですから、スマホは修理に出す前とまったく同じように使えます。
さて、ここで古いスマホと新しいスマホは「同じもの」だと言えるでしょうか。実は、それは考え方次第です。両方とも同じデータを持っていて同じように使えるのですから、使い勝手としては同じものだと言えるでしょう。でも、同じ機種でもハードウェアが違うのだから同じものではない、とも言えます。
1-2.意味上での同じと、実体が同じの違い
前節での考え方の差は、意味として同じであるかと、実体として同じであるかの差です。この差は、Javaでも「同じ」か判断をする時には、同様なことが言えます。
つまり、クラスから作られたインスタンスが同じであるか調べる時でも、インスタンスが持つ意味として同じである場合と、インスタンスの実体として同じである場合の二種類があるのです。
Javaで、意味として同じかを調べるのが、インスタンスメソッドObject.equalsです。インスタンスの実体として同じかを調べるのが、比較演算子==です。
1-3.equalsと==の違いをStringで体験しよう
equalsと==の違いが分かりやすいのは文字列、すなわちStringです。以下のプログラムを読んでみて、そしてぜひ実行してみてください。
String str1 = "ABCD"; String str2 = new String(new byte[] { 0x41, 0x42, 0x43, 0x44 }); // 数値での表現だが、"ABCD"と同じ意味 // 二つのStringが持つ文字列は見た目上では同じ System.out.println(str1); // → ABCD System.out.println(str2); // → ABCD // 二つのStringが持つ文字列が同じかequalsで確認する if (str1.equals(str2)) { System.out.println("str1 と str2 は同じ文字列です。"); // こちら!! } else { System.out.println("str1 と str2 は違う文字列です。"); } // 二つのStringのインスタンスが同じか==で確認する if (str1 == str2) { System.out.println("str1 と str2 は同じインスタンスです。"); } else { System.out.println("str1 と str2 は違うインスタンスです。"); // こちら!! }
変数str1とstr2は、両方とも“ABCD”という文字列を持つStringを指しています。前者のStringは文字列リテラルで作り、後者のStringはStringのインスタンスをbyte配列から新しく作っています。
この二つのStringは、それぞれのインスタンスがその内部に持っている文字列、すなわち値は同じです。ですから、str1とstr2が指すString同士をequalsで比べると、同じという答えが返ってきます。
しかし、Stringのインスタンスとしては別物です。str2が指すStringは新しく作っていますから、Javaのメモリ上には、二つのStringのインスタンスがそれぞれ違う実体としてあるのです。だから、str1とstr2を==で比べると、違うインスタンスを指しているという答えが返ってきます。
1-4.意味的に同じとは、持っている情報が同じということ
インスタンス同士が意味的に同じであるとは、具体的にはどういうことなのでしょうか。普通は、インスタンスが持つデータが同じであれば、意味的に同じとします。クラスとはデータ(=フィールド、メンバ変数)と手続き(=メソッド)が一つになったものですが、そのデータが同じかどうかです。
なぜかと言うと、インスタンスを特徴づけるのは持っているデータだからです。どういう経緯でインスタンスが作られようと、インスタンスが持っているデータが同じなら、メソッドを呼んだ時の振る舞いは同じになります。だから、同じデータを持つならば、同じと見なそうということにしているのです。
2.Object.equalsの使い方
2-1.equalsとは意味的に同じか確認するもの
Object.equalsは、すべてのクラスのスーパークラスであるObjectが持つ、自分自身と引数の何かのインスタンスが意味的に同じか調べるメソッドです。trueを戻せば同じ、falseを戻せば同じではない、ということです。
public boolean equals(Object obj) パラメータ: obj - 比較対象の参照オブジェクト。 戻り値: このオブジェクトがobj引数と同じである場合はtrue、それ以外の場合はfalse。
Object.equalsは抽象メソッドではないので、Objectでのデフォルト実装があります。Java 11での実装は以下のようになっています。つまり、自分自身と、引数のインスタンスの実体が同じかどうかを確認しています。
// Java 11のソースコードより抜粋 public boolean equals(Object obj) { return (this == obj); }
仮にクラスがデータを持たないのなら、同じインスタンスかどうかを判断する方法は、インスタンスとしての実体が同じかどうかしかない…ということです。Objectでのデフォルト実装はそういう意味なのです。
2-2-1.equalsはインスタンスに同じかを「聞く」メソッド
Object.equalsがインスタンスメソッドであることはとても印象的です。なぜなら「あなたはこれと『同じ』ですか?」とインスタンスに「聞く」メソッドであるからで、まさにオブジェクト指向的なアプローチです。
逆に、intやbooleanなどのプリミティブ型は、Javaではクラスから作られたインスタンスではなく、値そのものです。どんな状況であろうと、1ならすべて同じ1で、trueならすべて同じtrueです。だから、これらの値に「同じですか?」とわざわざ聞く必要はなく、いつでも==で値としての比較ができるのです。
2-2.equalsはサブクラスでオーバーライドする
どんなクラスでも必ずObjectのサブクラスですから、必ずequalsを呼び出せます。でも、サブクラスでオーバーライドしなければ、前述したObjectでのデフォルト処理が使われますので、実体が同じかどうかしかわかりません。
class EqualsSample1 { int id; // フィールドはあるが、同じかどうかの判定処理には使われていない… EqualsSample1(int id) { this.id = id; } public static void main(String[] args) { EqualsSample1 sample1 = new EqualsSample1(); EqualsSample1 sample2 = new EqualsSample1(); System.out.println(sample1.equals(sample2)); // → false System.out.println(sample1 == sample2); // → false } }
さすがにこれでは意味がないので、普通はサブクラスでequalsをオーバーライドして、引数のインスタンスが持つフィールドの値をチェックします。以下が、equalsをオーバーライドした例です。
class EqualsSample2 { int id; EqualsSample2(int id) { this.id = id; } public boolean equals(Object obj) { // EqualsSample2にキャストして、idの値が同じか調べる return id == ((EqualsSample2)obj).id; } public static void main(String[] args) { EqualsSample2 sample1 = new EqualsSample2(1); EqualsSample2 sample2 = new EqualsSample2(100); EqualsSample2 sample3 = new EqualsSample2(1); System.out.println(sample1.equals(sample2)); // → false System.out.println(sample1.equals(sample3)); // → true!! System.out.println(sample1 == sample2); // → false System.out.println(sample1 == sample3); // → false } }
この例では、フィールドのint idが同じかどうかを見ています。ですので、引数が同じ数字であるsample1とsample3が同じであるとequalsでは判断されています。もちろん、インスタンスとしては異なるので、比較演算子の結果は違うものだと言っています。
2-3.equalsでチェックすべき4つのポイント
さて、オーバーライドしたequalsではどんなことをチェックするべきでしょうか。equalsの引数の型はObjectなので、自分自身と同じクラスがいつも来るとは限りませんし、nullかどうかもチェックが必要そうですね。
equalsでは、以下の4つを順番にチェックするといいでしょう。
- 自分自身か:比較先のObjectが自分自身ならtrueで確定
- 比較先のObjectがnullか:nullならfalseで確定
- 自分のクラスと比較先のObjectのクラスが同じか:クラスが違えばfalseで確定
- 自分のフィールドと比較先のObjectのフィールドの内容が同じか:順番に比較する
以下は、この4つのチェックを行っているサンプルです。
import java.util.Objects; class EqualsSample3 { int id; String name; EqualsSample3(int id, String name) { this.id = id; this.name = name; } public boolean equals(Object obj) { // 1.自分自身か if (this == obj) { return true; } // 2.比較先のObjectがnullか // 3.自分のクラスと比較先のObjectのクラスが同じか // ※instanceofは左辺値がnullなら常にfalseなので、2/3を同時に行っています if (!(obj instanceof EqualsSample3)) { return false; } // 3.自分のフィールドと比較先のObjectのフィールドの内容が同じか EqualsSample3 other = (EqualsSample3)obj; // ①idが同じか if (id != other.id) { return false; } // ②nameが同じか if (!Objects.equals(name, other.name)) { return false; } return true; // 全部同じ!! thisとobjは同じもの!! } public static void main(String[] args) { EqualsSample3 sample1 = new EqualsSample3(1, "猫"); EqualsSample3 sample2 = new EqualsSample3(2, "猫"); EqualsSample3 sample3 = new EqualsSample3(1, "犬"); EqualsSample3 sample4 = new EqualsSample3(1, "猫"); System.out.println(sample1.equals(sample1)); // → true、「1.自分自身」に該当 System.out.println(sample1.equals(null)); // → false、「2.nullならfalse」に該当 System.out.println(sample1.equals("猫")); // → false、「3.クラスが違えばfalse」に該当 System.out.println(sample1.equals(sample2)); // → false、「4.フィールドが同じか」に該当(idが違う) System.out.println(sample1.equals(sample3)); // → false 「4.フィールドが同じか」に該当(nameが違う) System.out.println(sample1.equals(sample4)); // → true } }
2-4.フィールドの型ごとの比較の仕方
ここでは、フィールドの型ごとの比較の仕方を簡単にまとめます。参照型、配列型の記述はあくまで基本的な考え方なので、クラスに求められている比較条件で実装するようにしてください。
- boolean/byte/char/short/int/long/float/double: 比較演算子(==)で比較する
- 参照型: ①インスタンスが同じならtrueで確定、②null/not nullが合わない場合はfalseで確定、②参照型のequalsで比較する
- 配列型: ①インスタンスが同じならtrueで確定、②null/not nullが合わない場合はfalseで確定、③配列の長さが違えばfalseで確定、④配列が持つ値をすべて比較する
なお、参照型・配列型に記述した方法は、java.utilにあるObjects.equalsと、Arrays.equals/deepEqualsで実装されていますので、同じで良ければそれらのクラスを使いましょう。
Arrays.equals/deepEqualsは、プリミティブ型向けにオーバーロードされたメソッドがあります。多次元配列を扱う場合は、Arrays.deepEqualsが便利です。また、比較する配列のインデックスの範囲を指定できるものもありますよ。
import java.util.Objects; import java.util.Arrays; class EqualsSample4 { boolean booleanField; byte byteField; char charField; short shortField; int intField; long longField; float floatField; double doubleField; String objectField; String[] arrayField; public boolean equals(Object obj) { EqualsSample4 other = (EqualsSample4) obj; // booleanの場合は==で比較する if (booleanField != other.booleanField) { return false; } // byteの場合は==で比較する if (byteField != other.byteField) { return false; } // charの場合は==で比較する if (charField != other.charField) { return false; } // shortの場合は==で比較する if (shortField != other.shortField) { return false; } // intの場合は==で比較する if (intField != other.intField) { return false; } // longの場合は==で比較する if (longField != other.longField) { return false; } // floatの場合は==で比較する if (floatField != other.floatField) { return false; } // doubleの場合は==で比較する if (doubleField != other.doubleField) { return false; } // 参照型の場合は以下のロジックが代表的 if (objectField != other.objectField) { // ①参照先のインスタンスが違うならfalseで確定 return false; } else if (objectField == null && other.objectField != null) { // ②-1 null/not nullの組み合わせが一致しないならfalseで確定 return false; } else if (objectField != null && other.objectField == null) { // ②-2 null/not nullの組み合わせが一致しないならfalseで確定 return false; } else if (!objectField.equals(other.objectField)) { // ③インスタンス同士のequalsが違うならfalseで確定 return false; } /* // ↑は実質的にはObjects.equalsで代替できる if (!Objects.equals(objectField, other.objectField)) { return false; } */ // 配列型の場合は以下のロジックが代表的 if (arrayField != other.arrayField) { // ①参照先のインスタンスが違うならfalseで確定 return false; } else if (arrayField == null && other.arrayField != null) { // ②-1 null/not nullの組み合わせが一致しないならfalseで確定 return false; } else if (arrayField != null && other.arrayField == null) { // ②-2 null/not nullの組み合わせが一致しないならfalseで確定 return false; } else if (arrayField.length != other.arrayField.length) { // ③配列の長さが違うならfalseで確定 return false; } else { // ④配列の全要素に対して同じか確認する for (int i = 0; i < arrayField.length; i++) { // この例はString[]なのでnullとequalsでチェックする。プリミティブ型なら単に==で良い。 if (arrayField[i] == null && other.arrayField[i] == null) { continue; } else if (arrayField[i] == null && other.arrayField[i] != null) { return false; } else if (arrayField[i] != null && other.arrayField[i] == null) { return false; } else if (!arrayField[i].equals(other.arrayField[i])) { return false; } } } /* // ↑は実質的にはArrays.equalsで代替できる if (!Arrays.equals(arrayField, other.arrayField)) { return false; } */ return true; } }
3.【発展】equalsではどこまでチェックすべきか?
3-1.同じかどうかの判断に使う必要な範囲だけとする
equalsでどこまでチェックすべきかは、そのクラス次第です。そのインスタンスを他と識別するキーとなるフィールドだけでもいいですし、全フィールドをチェックしてもいいのです。つまり、チェックすべきフィールドはクラスの設計者が決めるのです。
また、フィールドの型が別のクラスの場合は、それら同士もequalsでチェックします。配列やListとして持っているデータも、チェックが必要ならば行います。つまり、自分自身を構成しているモノはすべてチェックの対象となりうるのです。でも、どこまでやるかは、これまた必要性次第です。
3-2.複数の比較ロジックがあるなら、equalsとは別のメソッドを作る
equalsを作る上で複数の選択肢がある場合、そのクラスのユースケースで何がより普通か、より自然かで判断します。例えば、データベース上のレコード(行)を表すクラスなら、①全列(=全フィールド)の一致を確認する、②主キーの列に相当するフィールドだけ確認する…の二つの大きな方針があり得ます。
通常使うequalsとは別に、特別なロジックで比較を行うequalsを作ってもよいでしょう。その場合は、equalsのオーバーロードではなく、別名のメソッドを新しく作ることをお勧めします。その方がequalsを別に作った意図が明確になるからです。この例は、String.equalsIgnoreCaseなどです。
3-3.equalsのオーバーロードはしない
引数がObjectのequalsをオーバーライドせずに違う型でオーバーロードするのは、バグの原因になりえますので行うべきではありません。「きちんとequalsを作ったのに動かないぞ」というケースの原因は、大体これです。
class EqualsSample5 { int id; EqualsSample4(int id) { this.id = id; } // 引数がEqualsSample5の、オーバーロードされたequals。 // 引数がObjectのものを、明示的にオーバーライドしていない。 public boolean equals(EqualsSample5 sample) { // idを使った比較ロジック } }
4.【発展】equalsに関するあれこれ
4-1.「違うか」を調べるなら否定演算子を使う
equalsは同じだということを調べますが、違うかどうかを知りたいなら、否定演算子!で逆転させればいいだけです。
String str1 = "ABCD"; String str2 = "EFGH"; if (!str1.equals(str2)) { System.out.println("str1とstr2は違うものです"); } else { System.out.println("str1とstr2は同じものです"); }
4-2.Comparable/Comparatorとequals
Comparable.compareToと、Comparator.compareは、二つのインスタンスを大小比較した結果、「同じ」であるなら0を戻すメソッドです。ここでも同じという考え方が出てきていますね。
Object.equalsでの確認結果と、Comparable/Comparatorでの確認結果は、同じにすることが「強く推奨」されています。つまり、equalsがtrueを戻せばcompareTo/compareは0を戻し、equalsがfalseを戻せばcomparaTo/compareは0ではない値を戻すべきだということです。
これを、compareToとequalsの一貫性と呼ぶこともあります。Object.equalsとComparable/Comparatorの結果が不一致だと、TreeSetやTreeMapなどのequalsとcompareToを積極的に活用しているクラスを使う時に、想定どおりの動きにならないなどの問題が発生しますので、気を付けましょう。
compareToの詳細は、以下の記事を参照してください。
関連記事4-3.Object.hashCodeとequals
インスタンスのハッシュ関数と呼べるものが、intを戻すObject.hashCodeです。hashCodeはインスタンスを特徴づける数字、ハッシュ値を返します。
hashCodeは、HashMapやHashSetの内部などで使われています。ですから、HashMapのキー、HashSetの値に使うクラスでhashCodeが適切に作られていなければ、これらのハッシュテーブルを使うクラスが予想どおりに動いてくれないのです。
equalsとhashCodeはお互いに深く関係していて、以下のルールを守るように作らなければなりません。
- equalsでの判断で使うフィールドが変わらないなら、hashCodeの値も変わらない
- equalsの結果がtrueなら、違うインスタンスでもhashCodeの値は同じになる
- equalsの結果がfalseなら、hashCodeの値は違わなくてもいい
hashCodeの詳細は、以下の記事を参照してください。
関連記事4-4.equalsを自動生成して楽をする
equalsで確認するフィールドが増えてくると、それらのフィールドごとの確認処理をきちんと作り込んで、動作確認をするだけでも一苦労です。ですが、IDEやライブラリを上手に使うと、きちんとしたequalsを簡単に自動生成できたりするのです!! ここで学んで、ぜひどんどん活用しましょう。
4-4-1.IDEの機能を使う
Eclipse、IntelliJ IDEA、NetBeansなどのJava向けIDEでは、equalsを自動生成してくれる機能があります。ここではEclipseでの例を示します。
例えば、以下のようなクラスがあったとします。このクラスにEclipseでequalsを自動生成してみましょう。
class EqualsSample6 { int intField; String strField; Object objField; }
このクラスをパッケージエクスプローラなどから、「右クリック」→「ソース」→「hashCode()およびequals()の生成」を選びます。すると「hashCode()およびequals()の生成」ウィザードが表示されるので、equalsとhashCodeの対象としたいフィールドを選び、OKボタンを押します。
すると、以下のようにequalsとhashCodeが自動生成されました。少々ごちゃごちゃしたソースコードですが、きちんと動きます。それに、実装しづらいhashCodeもequalsと同期を取って生成してくれるのは、大変助かりますね。
class EqualsSample6 { int intField; String strField; Object objField; @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + intField; result = prime * result + ((objField == null) ? 0 : objField.hashCode()); result = prime * result + ((strField == null) ? 0 : strField.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; EqualsSample6 other = (EqualsSample6) obj; if (intField != other.intField) return false; if (objField == null) { if (other.objField != null) return false; } else if (!objField.equals(other.objField)) return false; if (strField == null) { if (other.strField != null) return false; } else if (!strField.equals(other.strField)) return false; return true; } }
【参考】
IntelliJ IDEAの自動生成ウィザード
https://www.jetbrains.com/help/idea/generate-equals-and-hashcode-wizard.html
→[英語]IntelliJ IDEAの該当部分のマニュアルです。
NetBeansでの自動生成手順
https://blogs.oracle.com/java/ten-time-savers-in-netbeans-v2
→[英語]「Number 2: Auto-generate getters and setters, constructors and more!」のところに手順があります。
→[英語]equalsを自動する操作の簡単な手順が紹介されています。
4-4-2.ライブラリを使う(Lombokなど)
equalsはIDEに自動生成させる以外にも、外部ライブラリを使って自動生成する方法もあります。代表的なのは、Lombokというライブラリを使うことです。
Lombokの詳細や使えるようにする手順は省きますが、以下のようにクラスへアノテーション「@EqualsAndHashCode」を付けるだけで、クラスにあるフィールドを使ったequalsとhashCodeを自動的に作ってくれます!!(※) ものすごく楽ちんですね。
※Javaのソースコード上にはequalsとhashCodeは作成されず、コンパイルした結果のクラスファイル(.class)の中だけに、オーバーライドされたメソッドの実装が自動的に含まれます。
import lombok.EqualsAndHashCode; @EqualsAndHashCode public class EqualsSample4 { int intField; String strField; Object objField; }
【参考】
Project Lombok
→[英語]プロジェクトのWEBページです
@EqualsAndHashCode
https://projectlombok.org/features/EqualsAndHashCode
→[英語]公式WEBページ内の、@EqualsAndHashCodeの説明ページです。
5.まとめ
この記事では、Object.equalsを説明してきました。equalsはインスタンス同士が意味的に同じかを調べるメソッドで、クラスのフィールド同士を比較して同じ情報を持っているかを調べた結果を戻します。そして、比較演算子の==とは、使いどころが大きく違っているのです。
自分で作ったクラスでequalsをオーバーライドしていないと、色々なところで困ったことになります。オブジェクト指向プログラミング言語であるJavaでは、自分が他者と同じかを判断するのは自分自身がやるべきことであって、決して他人任せにはできない大事な処理です。
equalsを正しく作れば、Javaの標準APIにある色々なクラスを、正しく便利に使えるようになります。少し難しい考え方が求められるところもありますが、しっかりとポイントを押さえて活用できるようになりましょう。
ちなみに、もっと深くequalsを知りたい場合は、例えば書籍「Effective Java」に事細かく記述されていますので、そちらをご参照ください。そこに書かれていること理解すれば、あなたもすっかりequalsマスターですよ!!
コメント