入門equals() / hashCode() / toString()
-
equals()メソッド -
hashCode()メソッド -
toString()メソッド
これらはすべてObjectクラスに定義されたメソッドです。これらの意味を解説します。普段はあまり意識しませんが、けっこう大事なメソッドです。
環境
- JDK 21
equals()メソッドとは?
概要
equals()メソッド は、あるクラスの2つのインスタンスが「等しい」かどうかを判定します。どうなれば「等しい」かは、クラスによって変わってくるはずです。
例えば、
- 商品を表す
Productクラスでは、商品名nameが違っていても、商品IDidさえ等しければ同じ商品とみなす - 申請者を表す
Applicantクラスでは、氏名name・住所address・生年月日birthdayがすべて等しければ同じ申請者とみなす
などです。
実装の例
Productクラスはこんな感じで実装します。
package com.example;
import java.util.Objects;
public class Product {
private final String id;
private final String name;
public Product(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
// oがProductでなければfalse
if (!(o instanceof Product product)) {
return false;
}
// idが等しければtrue
return Objects.equals(id, product.id);
}
}
Objects.equals()についてはJavadocをご確認ください。
Applicantクラスはこんな感じで実装します。
package com.example;
import java.time.LocalDate;
import java.util.Objects;
public class Applicant {
private final String name;
private final String address;
private final LocalDate birthday;
public Applicant(String name, String address, LocalDate birthday) {
this.name = name;
this.address = address;
this.birthday = birthday;
}
@Override
public boolean equals(Object o) {
// oがApplicantでなければfalse
if (!(o instanceof Applicant applicant)) {
return false;
}
// name・address・birthdayがすべて等しければtrue
return Objects.equals(name, applicant.name)
&& Objects.equals(address, applicant.address)
&& Objects.equals(birthday, applicant.birthday);
}
}
こんな感じで確認してみます。
package com.example;
import java.time.LocalDate;
public class EqualsMain {
public static void main(String[] args) {
Product p1 = new Product("123", "商品A");
Product p2 = new Product("123", "商品B");
Product p3 = new Product("999", "商品A");
// 商品名は異なるが、商品IDが同じなのでtrue
System.out.println("p1.equals(p2): " + p1.equals(p2));
// 商品名は同じだが、商品IDが異なるのでfalse
System.out.println("p1.equals(p3): " + p1.equals(p3));
Applicant a1 = new Applicant("鈴木太郎", "東京都", LocalDate.of(1990, 1, 1));
Applicant a2 = new Applicant("鈴木太郎", "東京都", LocalDate.of(1990, 1, 1));
Applicant a3 = new Applicant("鈴木太郎", "東京都", LocalDate.of(2000, 1, 1));
// 氏名・住所・生年月日がすべて同じなのでtrue
System.out.println("a1.equals(a2): " + a1.equals(a2));
// 氏名・住所は同じだが、生年月日が異なるのでfalse
System.out.println("a1.equals(a3): " + a1.equals(a3));
}
}
p1.equals(p2): true
p1.equals(p3): false
a1.equals(a2): true
a1.equals(a3): false
==とequals()の違い
==は「同一インスタンスならtrue」になります。
package com.example;
public class EqualsMain2 {
public static void main(String[] args) {
Product p1 = new Product("123", "商品A");
Product p2 = p1; // p1とp2は同一インスタンスを指している
Product p3 = new Product("123", "商品A");
// 同一インスタンスなのでtrue
System.out.println("p1 == p2: " + (p1 == p2));
// 内容は等しいが、同一インスタンスではないのでtrue
System.out.println("p1 == p3: " + (p1 == p3));
// 同一インスタンスなので、当然内容も等しいのでtrue
System.out.println("p1.equals(p2): " + p1.equals(p2));
// 同一インスタンスではないが、内容は等しいのでtrue
System.out.println("p1.equals(p3): " + p1.equals(p3));
}
}
p1 == p2: true
p1 == p3: false
p1.equals(p2): true
p1.equals(p3): true
Objectクラスではどうなっている?
Objectクラスのequals()メソッドは、「同一インスタンスであればtrue」となります。つまり==と同じです。
ちなみに勘のいい方は「ProductやApplicantで使われているStringクラスやLocalDateクラスのequals()メソッドはどうなっているの?」と疑問に思ったのではないでしょうか。
実は、Stringクラスでは「文字列の内容が完全に一致すれば等しい」、LocalDateクラスでは「年・月・日の全てが等しければ等しい」となるように、equals()メソッドがオーバーライドされています。興味がある人は各クラスのソースコードを読んでみてください。
文字列の比較と==
文字列の比較は少しややこしいです。まずはコードを見てみましょう。
public class StringEqualsMain {
public static void main(String[] args) {
String str1 = "Hello";
String str2 = "Hello";
String str3 = new String("Hello");
System.out.println("str1.equals(str2): " + str1.equals(str2));
System.out.println("str1.equals(str3): " + str1.equals(str3));
System.out.println("str1 == str2: " + (str1 == str2));
System.out.println("str1 == str3: " + (str1 == str3));
}
}
str1.equals(str2): true
str1.equals(str3): true
str1 == str2: true
str1 == str3: false
str1 == str2がtrueになっていることに疑問をもつでしょう。実は、String str1 = "Hello"の時点で文字列"Hello"がメモリ上にキャッシュされ、str2にはそのキャッシュされた"Hello"が代入されます。なので、str1とstr2は同一インスタンスなのです。
一方、明示的にコンストラクタを呼び出して生成されたnew String("Hello")は、先ほどキャッシュされた"Hello"とは別のインスタンスとなります。なのでstr1とstr3は別インスタンスです。
なので文字列の比較には必ずequals()メソッドを使いましょう。しかし、equals()メソッドには次で説明する弱点があります。
equals()メソッドの弱点
equals()メソッドの弱点がnullです。次のようなコードの場合、equals()メソッドでNullPointerExceptionがスローされる可能性があります。
String s = hoge(); // nullの可能性あり
if (s.equals("xxx")) { // sがnullだとここで例外
...
}
以前はこれを避けるためのテクニックとして、次のように変数と比較対象値を逆にするテクニックが使われてきました。
String s = hoge(); // nullの可能性あり
if ("xxx".equals(s)) { // sがnullでも例外にならない!
...
}
しかし2つの変数同士の比較となると、例外を防止するには分岐を増やすしかありません。
String s1 = hoge(); // nullの可能性あり
String s2 = fuga(); // nullの可能性あり
if (s1 == null || s2 == null) { // 例外防止
...
} else if (s1.equals(s2)) {
...
}
対策: Objects.equals()で比較しよう
このような問題の解決策としてJava 7で登場したのがObjects.equals()メソッドです。
Objects.equals()メソッドは、引数で指定した2つの値を比較するメソッドです。
String s1 = hoge(); // nullの可能性あり
String s2 = fuga(); // nullの可能性あり
if (Objects.equals(s1, s2)) { // nullでも例外にならない!
...
}
Objects.equals()メソッドを使えば、nullの心配なく比較が可能です。
Objects.equals("a", "a") // true
Objects.equals("a", "b") // false
Objects.equals("a", null) // false
Objects.equals(null, "a") // false
ただし1点、null同士を比較するとtrueになることだけ注意してください。
Objects.equals(null, null) // true
このようにNullPointerExceptionの心配が無いため、2つのインスタンスの比較には必ずObjects.equals()メソッドを使いましょう。
hashCode()メソッドとは?
概要
hashCode()メソッドは、そのインスタンスのハッシュ値を返します。ハッシュ値は主にHashMapのキーで使われます。
ハッシュについては👇の記事を参照してください。
基本情報技術者試験受験ナビ サーチのアルゴリズム (3) ハッシュ表探索法のアルゴリズム
HashMapにmap.put(キーA, 値A)・map.put(キーB, 値B)のように値を格納する際、実はput()メソッド内部でキーのhashCode()メソッドを呼んでハッシュ値を計算しています。HashMapの内部にはハッシュ値ごとにキーと値のペアを保存する箱が用意されています。
例えばキーAのハッシュ値が1ならばキーAと値Aは1の箱に、キーBのハッシュ値が3ならばキーBと値Bは3の箱に保存されます。

この図はかなり簡略化しているので、正確な図ではありません。もっと詳しく知りたい方はYujiSoftwareさんの解説資料を読んでみてください!
hashCode()メソッドの規約
ObjectクラスのJavadocに、hashCode()メソッドの規約が書いてあります。ちょっと難しいので簡単に書くと
- あるインスタンス
xについて、equals()で利用するフィールドの値が変わっていなければ、hashCode()の値も変わらない。 - 2つのインスタンス
xとyについて、x.equals(y)がtrueならば、必ずx.hashCode() == y.hashCode()もtrueである。 - ただし
x.hashCode() == y.hashCode()がtrueであっても、x.equals(y)がtrueとは限らない。
equals()メソッドをオーバーライドした際は、 必ずhashCode()もこれらの規約を守りつつオーバーライドしてください。
実装の例
この規約に気をつけつつ、先ほどのProductクラス・ApplicantクラスにhashCode()メソッドを追加してみます。
package com.example;
import java.util.Objects;
public class Product {
private final String id;
private final String name;
public Product(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Product product)) {
return false;
}
return Objects.equals(id, product.id);
}
@Override
public int hashCode() {
// equals()でidのみを比較しているため、hashCodeもidのみを使用
return Objects.hash(id);
}
}
package com.example;
import java.time.LocalDate;
import java.util.Objects;
public class Applicant {
private final String name;
private final String address;
private final LocalDate birthday;
public Applicant(String name, String address, LocalDate birthday) {
this.name = name;
this.address = address;
this.birthday = birthday;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Applicant applicant)) {
return false;
}
return Objects.equals(name, applicant.name)
&& Objects.equals(address, applicant.address)
&& Objects.equals(birthday, applicant.birthday);
}
@Override
public int hashCode() {
// equals()でname, address, birthdayを比較しているため、hashCodeもそれらを使用
return Objects.hash(name, address, birthday);
}
}
Objects.hash()についてはJavadocをご確認ください。
Objectクラスではどうなっている?
ObjectクラスのhashCode()メソッドは、ネイティブコードで実装されています。そのためJavaのコードが書かれておらず、どうなっているのか分かりませんでした🫠
toString()メソッドとは?
概要
toString()メソッドは、インスタンスの文字列表現を返すメソッドです。特に規約は無いため、どのような文字列表現にするかは自由です。
実装の例
先ほどのProductクラス・ApplicantクラスにtoString()メソッドを追加してみます。
package com.example;
import java.util.Objects;
public class Product {
private final String id;
private final String name;
public Product(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Product product)) {
return false;
}
return Objects.equals(id, product.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return "商品情報: 商品ID=" + id + ", 商品名=" + name;
}
}
package com.example;
import java.time.LocalDate;
import java.util.Objects;
public class Applicant {
private final String name;
private final String address;
private final LocalDate birthday;
public Applicant(String name, String address, LocalDate birthday) {
this.name = name;
this.address = address;
this.birthday = birthday;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Applicant applicant)) {
return false;
}
return Objects.equals(name, applicant.name)
&& Objects.equals(address, applicant.address)
&& Objects.equals(birthday, applicant.birthday);
}
@Override
public int hashCode() {
return Objects.hash(name, address, birthday);
}
@Override
public String toString() {
return "申請者情報: 氏名=" + name
+ ", 住所=" + address
+ ", 生年月日=" + birthday;
}
}
System.out.println()にインスタンスを渡すと、内部でtoString()メソッドが呼ばれ、その文字列が表示されます。
package com.example;
import java.time.LocalDate;
public class ToStringMain {
public static void main(String[] args) {
Product p1 = new Product("123", "商品A");
System.out.println(p1);
Applicant a1 = new Applicant("鈴木太郎", "東京都", LocalDate.of(1990, 1, 1));
System.out.println(a1);
}
}
商品情報: 商品ID=123, 商品名=商品A
申請者情報: 氏名=鈴木太郎, 住所=東京都, 生年月日=1990-01-01
Objectクラスではどうなっている?
ObjectクラスのtoString()メソッドは、クラスの完全修飾名@16進数形式のハッシュコード(例: java.lang.Object@7cc355be )という文字列を返すようになっています。
レコードクラスではどうなっている?
レコードクラスでは、equals()メソッド・hashCode()メソッド・toString()メソッドがすべてオーバーライド済みの状態になっています。
-
equals()メソッド- 全コンポーネント(≒フィールド)が等しければ
trueを返すようになっています。
- 全コンポーネント(≒フィールド)が等しければ
-
hashCode()メソッド- 全コンポーネントのハッシュ値を利用して計算されます。
-
toString()メソッドは-
単純クラス名[コンポーネント名1=値1, コンポーネント名2=値2, ...]という文字列を返すようになっています。
-
package com.example;
public record Employee(String name, int age) {}
package com.example;
public class RecordMain {
public static void main(String[] args) {
Employee e1 = new Employee("Alice", 30);
Employee e2 = new Employee("Alice", 30);
System.out.println("e1.equals(p2): " + e1.equals(e2));
System.out.println("e1.hashCode(): " + e1.hashCode());
System.out.println(e1);
}
}
e1.equals(p2): true
e1.hashCode(): 1963861438
Employee[name=Alice, age=30]
Discussion
これですが、オブジェクトごとに乱数を返すように実装されています。
java.lang.Object#hashCode()の性質
おおなるほど!コメントありがとうございます〜😆