☕️

Javaのレコードについてメモ:equals/hashCode/toString

2021/09/16に公開

Java 17がリリースされたのでレコードについて軽く調べてメモっておきます。
とりあえず書きたくなったのはequals/hashCode/toStringについてです。

環境

$ java --version
java 17 2021-09-14 LTS
Java(TM) SE Runtime Environment (build 17+35-LTS-2724)
Java HotSpot(TM) 64-Bit Server VM (build 17+35-LTS-2724, mixed mode, sharing)

バイトコードを見てみる

まず、こんな感じのレコードを作ります。

public record Person(String name, int age) {
}

これをコンパイルしてからjavapするとhashCodeメソッドは次のようにindyが使われていました。

  public final int hashCode();
    descriptor: ()I
    flags: (0x0011) ACC_PUBLIC, ACC_FINAL
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokedynamic #21,  0             // InvokeDynamic #0:hashCode:(LPerson;)I
         6: ireturn
      LineNumberTable:
        line 1: 0

equalstoStringも同じようにindyを使ったバイトコードになっていました。
ただ、呼び出されているメソッドのコードがどこにあるのかはわかりませんでした。

仕様を確認

8.10.3. Record Membersequals/hashCode/toStringの仕様が書かれていました。

equalsthisと引数が同じコンポーネントを持っているかチェックします。
コンポーネントが参照型の場合はコンポーネントのequalsを呼び出します。
コンポーネントがプリミティブの場合はラッパークラスのcompareを呼び出して、戻り値が0の場合は同値とします。

hashCodeはすべてのコンポーネントのハッシュ値を計算します。
コンポーネントが参照型の場合はコンポーネントのhashCodeを呼び出します。
コンポーネントがプリミティブの場合はボクシングしてラッパークラスのhashCodeを呼び出します。
各コンポーネントのハッシュ値をどう合わせるのかは記載が見当たりませんでした。

toStringはレコードの型名と各コンポーネントの文字列表現を組み合わせます。
コンポーネントが参照型の場合はコンポーネントのtoStringを呼び出します。
コンポーネントがプリミティブの場合はボクシングしてラッパークラスのtoStringを呼び出します。

toStringすると、こんな感じになりました。

Person[name=緋村剣心, age=28]

簡単に実装を確認

スタックトレースを使うとどんな実装かわかるかもと思い、次のようなJavaプログラムを書いて実行しました。

public class App {

    public static void main(String[] args) {
        Foo foo1 = new Foo(new Bar());
        Foo foo2 = new Foo(new Bar());

        System.out.println("[equals]");
        foo1.equals(foo2);

        System.out.println("[hashCode]");
        foo1.hashCode();

        System.out.println("[toString]");
        foo1.toString();
    }

    record Foo(Bar bar) {
    }

    static class Bar {

        @Override
        public int hashCode() {
            new Throwable().printStackTrace(System.out);
            return 0;
        }

        @Override
        public boolean equals(Object obj) {
            new Throwable().printStackTrace(System.out);
            return true;
        }

        @Override
        public String toString() {
            new Throwable().printStackTrace(System.out);
            return "";
        }
    }
}

出力は次の通りです。

[equals]
java.lang.Throwable
        at App$Bar.equals(App.java:30)
        at java.base/java.util.Objects.equals(Objects.java:64)
        at App$Foo.equals(App.java:17)
        at App.main(App.java:8)
[hashCode]
java.lang.Throwable
        at App$Bar.hashCode(App.java:24)
        at java.base/java.util.Objects.hashCode(Objects.java:103)
        at App$Foo.hashCode(App.java:17)
        at App.main(App.java:11)
[toString]
java.lang.Throwable
        at App$Bar.toString(App.java:36)
        at java.base/java.lang.String.valueOf(String.java:4215)
        at java.base/java.util.Objects.toString(Objects.java:147)
        at App$Foo.toString(App.java:17)
        at App.main(App.java:14)

java.util.Objectsが使われていました。

以上です。

Discussion