Javaについて徹底解説!

インスタンスが「同じ」かチェック!! Javaのequalsを基礎から解説

大石 英人

開発エンジニア/Java20年/Java GOLD/リーダー/ボールド歴2年

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です。以下のプログラムを読んでみて、そしてぜひ実行してみてください。

変数str1str2は、両方とも“ABCD”という文字列を持つStringを指しています。前者のStringは文字列リテラルで作り、後者のStringStringのインスタンスをbyte配列から新しく作っています。

この二つのStringは、それぞれのインスタンスがその内部に持っている文字列、すなわち値は同じです。ですから、str1str2が指すString同士をequalsで比べると、同じという答えが返ってきます。

しかし、Stringのインスタンスとしては別物です。str2が指すStringは新しく作っていますから、Javaのメモリ上には、二つのStringのインスタンスがそれぞれ違う実体としてあるのです。だから、str1str2==で比べると、違うインスタンスを指しているという答えが返ってきます。

1-4.意味的に同じとは、持っている情報が同じということ

インスタンス同士が意味的に同じであるとは、具体的にはどういうことなのでしょうか。普通は、インスタンスが持つデータが同じであれば、意味的に同じとします。クラスとはデータ(=フィールド、メンバ変数)と手続き(=メソッド)が一つになったものですが、そのデータが同じかどうかです。

なぜかと言うと、インスタンスを特徴づけるのは持っているデータだからです。どういう経緯でインスタンスが作られようと、インスタンスが持っているデータが同じなら、メソッドを呼んだ時の振る舞いは同じになります。だから、同じデータを持つならば、同じと見なそうということにしているのです。


2.Object.equalsの使い方

2-1.equalsとは意味的に同じか確認するもの

Object.equalsは、すべてのクラスのスーパークラスであるObjectが持つ、自分自身と引数の何かのインスタンスが意味的に同じか調べるメソッドです。trueを戻せば同じ、falseを戻せば同じではない、ということです。

Object.equalsは抽象メソッドではないので、Objectでのデフォルト実装があります。Java 11での実装は以下のようになっています。つまり、自分自身と、引数のインスタンスの実体が同じかどうかを確認しています。

仮にクラスがデータを持たないのなら、同じインスタンスかどうかを判断する方法は、インスタンスとしての実体が同じかどうかしかないということです。Objectでのデフォルト実装はそういう意味なのです。

2-2-1.equalsはインスタンスに同じかを「聞く」メソッド

Object.equalsがインスタンスメソッドであることはとても印象的です。なぜなら「あなたはこれと『同じ』ですか?」とインスタンスに「聞く」メソッドであるからで、まさにオブジェクト指向的なアプローチです。

逆に、intbooleanなどのプリミティブ型は、Javaではクラスから作られたインスタンスではなく、値そのものです。どんな状況であろうと、1ならすべて同じ1で、trueならすべて同じtrueです。だから、これらの値に「同じですか?」とわざわざ聞く必要はなく、いつでも==で値としての比較ができるのです。

2-2.equalsはサブクラスでオーバーライドする

どんなクラスでも必ずObjectのサブクラスですから、必ずequalsを呼び出せます。でも、サブクラスでオーバーライドしなければ、前述したObjectでのデフォルト処理が使われますので、実体が同じかどうかしかわかりません。

さすがにこれでは意味がないので、普通はサブクラスでequalsをオーバーライドして、引数のインスタンスが持つフィールドの値をチェックします。以下が、equalsをオーバーライドした例です。

この例では、フィールドのint idが同じかどうかを見ています。ですので、引数が同じ数字であるsample1sample3が同じであるとequalsでは判断されています。もちろん、インスタンスとしては異なるので、比較演算子の結果は違うものだと言っています。

2-3.equalsでチェックすべき4つのポイント

さて、オーバーライドしたequalsではどんなことをチェックするべきでしょうか。equalsの引数の型はObjectなので、自分自身と同じクラスがいつも来るとは限りませんし、nullかどうかもチェックが必要そうですね。

equalsでは、以下の4つを順番にチェックするといいでしょう。

  1. 自分自身か:比較先のObjectが自分自身ならtrueで確定
  2. 比較先のObjectnullか:nullならfalseで確定
  3. 自分のクラスと比較先のObjectのクラスが同じか:クラスが違えばfalseで確定
  4. 自分のフィールドと比較先のObjectのフィールドの内容が同じか:順番に比較する

以下は、この4つのチェックを行っているサンプルです。

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が便利です。また、比較する配列のインデックスの範囲を指定できるものもありますよ。


3.【発展】equalsではどこまでチェックすべきか?

3-1.同じかどうかの判断に使う必要な範囲だけとする

equalsでどこまでチェックすべきかは、そのクラス次第です。そのインスタンスを他と識別するキーとなるフィールドだけでもいいですし、全フィールドをチェックしてもいいのです。つまり、チェックすべきフィールドはクラスの設計者が決めるのです。

また、フィールドの型が別のクラスの場合は、それら同士もequalsでチェックします。配列やListとして持っているデータも、チェックが必要ならば行います。つまり、自分自身を構成しているモノはすべてチェックの対象となりうるのです。でも、どこまでやるかは、これまた必要性次第です。

3-2.複数の比較ロジックがあるなら、equalsとは別のメソッドを作る

equalsを作る上で複数の選択肢がある場合、そのクラスのユースケースで何がより普通か、より自然かで判断します。例えば、データベース上のレコード()を表すクラスなら、全列(=全フィールド)の一致を確認する、主キーの列に相当するフィールドだけ確認するの二つの大きな方針があり得ます。

通常使うequalsとは別に、特別なロジックで比較を行うequalsを作ってもよいでしょう。その場合は、equalsのオーバーロードではなく、別名のメソッドを新しく作ることをお勧めします。その方がequalsを別に作った意図が明確になるからです。この例は、String.equalsIgnoreCaseなどです。

3-3.equalsのオーバーロードはしない

引数がObjectequalsをオーバーライドせずに違う型でオーバーロードするのは、バグの原因になりえますので行うべきではありません。「きちんとequalsを作ったのに動かないぞ」というケースの原因は、大体これです。


4.【発展】equalsに関するあれこれ

4-1.「違うか」を調べるなら否定演算子を使う

equalsは同じだということを調べますが、違うかどうかを知りたいなら、否定演算子!で逆転させればいいだけです。

4-2.Comparable/Comparatorとequals

Comparable.compareToと、Comparator.compareは、二つのインスタンスを大小比較した結果、「同じ」であるなら0を戻すメソッドです。ここでも同じという考え方が出てきていますね。

Object.equalsでの確認結果と、Comparable/Comparatorでの確認結果は、同じにすることが「強く推奨」されています。つまり、equalstrueを戻せばcompareTo/compare0を戻し、equalsfalseを戻せばcomparaTo/compare0ではない値を戻すべきだということです。

これを、compareToequalsの一貫性と呼ぶこともあります。Object.equalsComparable/Comparatorの結果が不一致だと、TreeSetTreeMapなどのequalscompareToを積極的に活用しているクラスを使う時に、想定どおりの動きにならないなどの問題が発生しますので、気を付けましょう。

compareToの詳細は、以下の記事を参照してください。

関連記事

4-3.Object.hashCodeとequals

インスタンスのハッシュ関数と呼べるものが、intを戻すObject.hashCodeです。hashCodeはインスタンスを特徴づける数字、ハッシュ値を返します。

hashCodeは、HashMapHashSetの内部などで使われています。ですから、HashMapのキー、HashSetの値に使うクラスでhashCodeが適切に作られていなければ、これらのハッシュテーブルを使うクラスが予想どおりに動いてくれないのです。

equalsとhashCodeはお互いに深く関係していて、以下のルールを守るように作らなければなりません。

  1. equalsでの判断で使うフィールドが変わらないなら、hashCodeの値も変わらない
  2. equalsの結果がtrueなら、違うインスタンスでもhashCodeの値は同じになる
  3. equalsの結果がfalseなら、hashCodeの値は違わなくてもいい

hashCodeの詳細は、以下の記事を参照してください。

関連記事

4-4.equalsを自動生成して楽をする

equalsで確認するフィールドが増えてくると、それらのフィールドごとの確認処理をきちんと作り込んで、動作確認をするだけでも一苦労です。ですが、IDEやライブラリを上手に使うと、きちんとしたequalsを簡単に自動生成できたりするのです!! ここで学んで、ぜひどんどん活用しましょう。

4-4-1.IDEの機能を使う

Eclipse、IntelliJ IDEANetBeansなどのJava向けIDEでは、equalsを自動生成してくれる機能があります。ここではEclipseでの例を示します。

例えば、以下のようなクラスがあったとします。このクラスにEclipseequalsを自動生成してみましょう。

このクラスをパッケージエクスプローラなどから、「右クリック」「ソース」hashCode()およびequals()の生成」を選びます。すると「hashCode()およびequals()の生成」ウィザードが表示されるので、equalshashCodeの対象としたいフィールドを選び、OKボタンを押します。

すると、以下のようにequalshashCodeが自動生成されました。少々ごちゃごちゃしたソースコードですが、きちんと動きます。それに、実装しづらいhashCodeequalsと同期を取って生成してくれるのは、大変助かりますね。

        【参考】

        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!」のところに手順があります。

 

        https://stackoverflow.com/questions/38242576/can-netbeans-auto-generate-correct-hashcode-and-equals-methods-for-a-mapping

        →[英語]equalsを自動する操作の簡単な手順が紹介されています。

4-4-2.ライブラリを使う(Lombokなど)

equalsはIDEに自動生成させる以外にも、外部ライブラリを使って自動生成する方法もあります。代表的なのは、Lombokというライブラリを使うことです。

Lombokの詳細や使えるようにする手順は省きますが、以下のようにクラスへアノテーション「@EqualsAndHashCode」を付けるだけで、クラスにあるフィールドを使ったequalshashCodeを自動的に作ってくれます!!(※) ものすごく楽ちんですね。

※Javaのソースコード上にはequalshashCodeは作成されず、コンパイルした結果のクラスファイル(.class)の中だけに、オーバーライドされたメソッドの実装が自動的に含まれます。

        【参考】

        Project Lombok

        https://projectlombok.org/

        →[英語]プロジェクトのWEBページです

 

        @EqualsAndHashCode

        https://projectlombok.org/features/EqualsAndHashCode

        →[英語]公式WEBページ内の、@EqualsAndHashCodeの説明ページです。


5.まとめ

この記事では、Object.equalsを説明してきました。equalsはインスタンス同士が意味的に同じかを調べるメソッドで、クラスのフィールド同士を比較して同じ情報を持っているかを調べた結果を戻します。そして、比較演算子の==とは、使いどころが大きく違っているのです。

自分で作ったクラスでequalsをオーバーライドしていないと、色々なところで困ったことになります。オブジェクト指向プログラミング言語であるJavaでは、自分が他者と同じかを判断するのは自分自身がやるべきことであって、決して他人任せにはできない大事な処理です。

equalsを正しく作れば、Javaの標準APIにある色々なクラスを、正しく便利に使えるようになります。少し難しい考え方が求められるところもありますが、しっかりとポイントを押さえて活用できるようになりましょう。

ちなみに、もっと深くequalsを知りたい場合は、例えば書籍「Effective Java」に事細かく記述されていますので、そちらをご参照ください。そこに書かれていること理解すれば、あなたもすっかりequalsマスターですよ!!

私たちは、全てのエンジニアに市場価値を高め自身の望む理想のキャリアを歩んでいただきたいと考えています。もし、今あなたが転職を検討しているのであればこちらの記事をご一読ください。理想のキャリアを実現するためのヒントが見つかるはずです。

8/28(水)開催決定!
【テックジム×ENGINEER.CLUB】ゼロからはじめるPythonプログラミング入門講座

プログラミングは初めてだけどPythonから始めてみたいという方のために、無料のハンズオン開発講座をテックジム×ENGINEER.CLUBで共同開催することになりました。開催日時は以下の通りです。

  • 8月28日(水)19時〜21時 東京開催

本講座で学んでいただく「TechGYM方式」とは、基礎知識なしでも座学なしでプログラミングに専念できるように設計されたプログラミングのカリキュラムメソッドです。「まるで魔法にかかったようにプログラミンスキルが習得できる」と評判の本講座をぜひ一度体験してみてください。

『技術力』と『人間力』を高め市場価値の高いエンジニアを目指しませんか?

私たちは「技術力」だけでなく「人間力」の向上をもって遙かに高い水準の成果を出し、関わる全ての人々に感動を与え続ける集団でありたいと考えています。

高い水準で仕事を進めていただくためにも、弊社では次のような環境を用意しています。

  • 定年までIT業界で働くためのスキル(技術力、人間力)が身につく支援
  • 「給与が上がらない」を解消する6ヶ月に1度の明確な人事評価制度
  • 平均残業時間17時間!毎週の稼動確認を徹底しているから実現できる働きやすい環境

現在、株式会社ボールドでは「キャリア採用」のエントリーを受付中です。

まずは以下のボタンより弊社の紹介をご覧いただき、あなたの望むキャリアビジョンをエントリーフォームより詳しくお聞かせください。