技術日誌

DB,Java,セキュリティ,機械学習など。興味のあることを雑多に学ぶ

Kotlinのデータクラスが生成するhashCode()の謎に逆アセンブラで迫る

data class Person(val name:String, val age: Int)

データクラスを逆コンパイルしたところhashCodeのソースが作れませんでした。

$ jad Person.class 
Parsing Person.class... Generating Person.jad
Couldn't fully decompile method hashCode

jadで逆コンパイルできなかったのでアセンブラからhashCodeの動きをトレースする試みです。

    var bob =  Person("Bob",33)//コンストラクタの解説は省略
    println(bob.hashCode())//2075948//これを調べます

アセンブラコンパイラの出力

jadの出力

    public int hashCode()
    {
        name;
        if(name == null) goto _L2; else goto _L1
_L1:
        hashCode();
          goto _L3
_L2:
        JVM INSTR pop ;
        false;
_L3:
        31;
        JVM INSTR imul ;
        age;
        JVM INSTR iadd ;
        return;
    }

javapの出力

public int hashCode();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #11                 // Field name:Ljava/lang/String;
         4: dup
         5: ifnull        14
         8: invokevirtual #63                 // Method java/lang/Object.hashCode:()I
        11: goto          16
        14: pop
        15: iconst_0
        16: bipush        31
        18: imul
        19: aload_0
        20: getfield      #19                 // Field age:I
        23: iadd
        24: ireturn
      StackMapTable: number_of_entries = 2
        frame_type = 78 /* same_locals_1_stack_item */
          stack = [ class java/lang/String ]
        frame_type = 65 /* same_locals_1_stack_item */
          stack = [ int ]

命令

dup

Java Virtual Machine - Online Reference - dup

スタックの最上位をそのままスタックに再度積む。

aloud_n

Java Virtual Machine - Online Reference - aload_<n>

ローカル変数0に入っている参照をスタックに積む。

invokevirtual

インスタンスメソッドを呼びます

Java Virtual Machine - Online Reference - invokevirtual

invokevirtualは<method-spec>で指定された記述子を調べ、メソッドが取る引数の数を決定します(これはゼロかもしれません)。これらの引数をオペランドスタックからポップします。次に、スタックからオブジェクト参照をポップします。 objectrefは、メソッドが呼び出されているオブジェクトへの参照です。 invokevirtualはobjectrefのJavaクラスを取得し、そのクラスとそのスーパークラスによって定義されたメソッドのリストを検索し、記述子が記述子であるmethodnameというメソッドを探します。

ifnull

スタックの先頭がnullであればラベルまで飛ぶ

Java Virtual Machine - Online Reference - ifnull

I_const_0

定数0をスタックに積む

Java Virtual Machine - Online Reference - iconst_<n>

bipush

1バイトの指定した数値(-128~127)をスタックに積む

Java Virtual Machine - Online Reference - bipush

imul

Java Virtual Machine - Online Reference - imul

スタックの最上位2つを使って掛け算

iadd

Java Virtual Machine - Online Reference - iadd

スタックの最上位2つを使って足し算。

アセンブラの動きをトレースして見ます

0: aload_0

stack
Persionのインスタンス参照(bob)

1: getfield #11

stack
nameのStringオブジェクトへの参照("bob")

4: dup

stack
nameのStringオブジェクトへの参照("bob")
nameのStringオブジェクトへの参照("bob")

5: ifnull 14

stack
nameのStringオブジェクトへの参照("bob")

note:プライマリコンストラクタ宣言時にnameの形をnullableにしていないため nameがnullになることはないためそのまま8に進みます

8: invokevirtual #63

hashCode()のインスタンスメソッドを実行します 対象となるインスタンスはもちろん"bob"です

試しにKotlinで"bob".hashCode()を実行したら66965となったので そのままスタックに入れて見ます

stack
66965

11: goto 16

nameがnull出なかった時は14,15を飛ばすための処理。 16に飛びます。

14: pop

この命令は実行はされませんが、 nameがnullでない場合、name.hashCode実行時にstackからname(今回は"bob")の参照がpopされますが、 nullだと実行されず残ってしまうのでここでpopして消しています。

stack

15: iconst_0

0をstackに積みます

stack
0

16: bipush 31

31をスタックに積みます

stack
66965
31

18: imul

スタックの内容で掛け算をします

66965  * 31 = 2075915
stack
2075915

19: aload_0

Persionのインスタンスの参照をスタックに積みます

stack
Persionのインスタンス参照(bob)
2075915

20: getfield #19

ageの値をスタックに積みます

stack
33
2075915

23: iadd

33 + 2075915 = 2075948

stack
2075948

24: ireturn

hashcodeの結果としてstackの最上位のもの(2075948)をreturnします。

結論

というわけで、正解は

nameのhashCodeに31をかけたものにageを足したものでした。 時間かけた割にあまり面白いものではなかった・・・ 同じメンバのクラスを作ってEclipseでhashCode()の自動生成をしたものと微妙に違っていますね。

   @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + age;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

これくらい逆コンパイルしてくれても良さそうなんだけど、できないのでしょうか?

おまけ:Kotlinで同じことをやってみる

    var bob =  Person("Bob",33)
    println("Bob".hashCode())//66965
    println("Bob".hashCode() * 31)//2075915
    println("Bob".hashCode() * 31 + 33)//2075948
    println(bob.hashCode())//2075948