入門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()
メソッドを使いましょう。そうすれば間違いありません。
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()の性質
おおなるほど!コメントありがとうございます〜😆