📘

入門equals() / hashCode() / toString()

に公開
2
  • equals()メソッド
  • hashCode()メソッド
  • toString()メソッド

これらはすべてObjectクラスに定義されたメソッドです。これらの意味を解説します。普段はあまり意識しませんが、けっこう大事なメソッドです。

環境

  • JDK 21

equals()メソッドとは?

概要

equals()メソッド は、あるクラスの2つのインスタンスが「等しい」かどうかを判定します。どうなれば「等しい」かは、クラスによって変わってくるはずです。

例えば、

  • 商品を表すProductクラスでは、商品名nameが違っていても、商品IDidさえ等しければ同じ商品とみなす
  • 申請者を表すApplicantクラスでは、氏名name・住所address・生年月日birthdayがすべて等しければ同じ申請者とみなす

などです。

実装の例

Productクラスはこんな感じで実装します。

Product.java
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クラスはこんな感じで実装します。

Applicant.java
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);
  }
}

こんな感じで確認してみます。

EqualsMain.java
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」になります。

EqualsMain2.java
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」となります。つまり==と同じです。

ちなみに勘のいい方は「ProductApplicantで使われているStringクラスやLocalDateクラスのequals()メソッドはどうなっているの?」と疑問に思ったのではないでしょうか。

実は、Stringクラスでは「文字列の内容が完全に一致すれば等しい」、LocalDateクラスでは「年・月・日の全てが等しければ等しい」となるように、equals()メソッドがオーバーライドされています。興味がある人は各クラスのソースコードを読んでみてください。

文字列の比較と==

文字列の比較は少しややこしいです。まずはコードを見てみましょう。

StringEqualsMain.java
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 == str2trueになっていることに疑問をもつでしょう。実は、String str1 = "Hello"の時点で文字列"Hello"がメモリ上にキャッシュされ、str2にはそのキャッシュされた"Hello"が代入されます。なので、str1str2は同一インスタンスなのです。

一方、明示的にコンストラクタを呼び出して生成されたnew String("Hello")は、先ほどキャッシュされた"Hello"とは別のインスタンスとなります。なのでstr1str3は別インスタンスです。

この辺りが少しややこしいので、文字列同士の比較は必ずequals()メソッドを使いましょう。そうすれば間違いありません。

hashCode()メソッドとは?

概要

hashCode()メソッドは、そのインスタンスのハッシュ値を返します。ハッシュ値は主にHashMapのキーで使われます。

ハッシュについては👇の記事を参照してください。
基本情報技術者試験受験ナビ サーチのアルゴリズム (3) ハッシュ表探索法のアルゴリズム

HashMapmap.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()メソッドの規約が書いてあります。ちょっと難しいので簡単に書くと

  1. あるインスタンスxについて、equals()で利用するフィールドの値が変わっていなければ、hashCode()の値も変わらない。
  2. 2つのインスタンスxyについて、x.equals(y)trueならば、必ずx.hashCode() == y.hashCode()trueである。
  3. ただしx.hashCode() == y.hashCode()trueであっても、x.equals(y)trueとは限らない。

equals()メソッドをオーバーライドした際は、 必ずhashCode()もこれらの規約を守りつつオーバーライドしてください。

実装の例

この規約に気をつけつつ、先ほどのProductクラス・ApplicantクラスにhashCode()メソッドを追加してみます。

Product.java
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);
  }
}
Applicant.java
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()メソッドを追加してみます。

Product.java
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;
  }
}
Applicant.java
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()メソッドが呼ばれ、その文字列が表示されます。

ToStringMain.java
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, ...]という文字列を返すようになっています。
RecordMain.java
package com.example;

public record Employee(String name, int age) {}
RecordMain.java
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

YujiSoftwareYujiSoftware

ObjectクラスのhashCode()メソッドは、ネイティブコードで実装されています。そのためJavaのコードが書かれておらず、どうなっているのか分かりませんでした🫠

これですが、オブジェクトごとに乱数を返すように実装されています。
java.lang.Object#hashCode()の性質