KotlinプロパティとカスタムゲッターのBytecodeから見る違い
こんにちは、tkyです。
今日はKotlinのプロパティとゲッターの種類と違いを深堀りしてみようと思います。
はじめにプロパティとフィールドについておさらい程度に単語を整理します。
プロパティとフィールド
Kotlinのプロパティについて復習です。
Kotlinにおいて以下のように記述することはプロパティを宣言することであって、暗黙的にゲッターセッターを持っています。
Javaにするとフィールドとゲッターセッターを記述する必要があるのに対してボイラーコードが減っていいですね。
// kotlin var name: String = "This is a property"
// convert to java private String name = "This is a field."; public String getName() { return name; } public void setName(String var1){ name = var1; }
さらにKotlinのプロパティは明示的にゲッターを記述することもできます。
val isEmpty: Boolean get() = this.size == 0
そして fun getName()
のように宣言しようとするとコンパイルエラーとなります。プロパティ宣言によって暗黙的にgetName()メソッドが宣言されていることが確認できます。
ここまではある程度みなさんも理解していることでしょう。
ゲッター書き方色々
1つのプロパティにアクセスする方法がいくつか存在します。特に暗黙的に作成されたゲッターと明示的に作成されたゲッターは何が違うのでしょうか?
- プロパティ(暗黙的なゲッターを利用)
- カスタムゲッター(明示的に記述する)
- メソッド(暗黙的なゲッターとは異なる命名で作成)
この謎を解明すべくByteCodeという名の秘境の奥地に足を踏み入れてみたのでした。
サンプルコード
適当に作ってみました。全部valで定義しているのでセッターはありません。
class User(val firstName: String, val lastName: String, val age: Int) { // プロパティ宣言 val fullName: String = "$firstName $lastName" // カスタムゲッター val fullNameCustom: String get() = "$firstName $lastName" // メソッドでゲッター fun getFullNameMethod(): String { return "$firstName $lastName" } }
バイトコード
Android Studioの機能で Tools > Kotlin > Show Kotlin Bytecode を選択することでBytecodeが見れるようになります。
長すぎたので要所だけ切り取ります。
// プロパティ宣言 public final getFullName()Ljava/lang/String; @Lorg/jetbrains/annotations/NotNull;() // invisible L0 LINENUMBER 6 L0 ALOAD 0 GETFIELD com/github/ticktakclock/myapplication/User.fullName : Ljava/lang/String; ARETURN L1 // カスタムゲッター public final getFullNameCustom()Ljava/lang/String; @Lorg/jetbrains/annotations/NotNull;() // invisible L0 LINENUMBER 10 L0 NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V ALOAD 0 GETFIELD com/github/ticktakclock/myapplication/User.firstName : Ljava/lang/String; INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; BIPUSH 32 INVOKEVIRTUAL java/lang/StringBuilder.append (C)Ljava/lang/StringBuilder; ALOAD 0 GETFIELD com/github/ticktakclock/myapplication/User.lastName : Ljava/lang/String; INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; ARETURN L1 // メソッドでゲッター public final getFullNameMethod()Ljava/lang/String; @Lorg/jetbrains/annotations/NotNull;() // invisible L0 LINENUMBER 14 L0 NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V ALOAD 0 GETFIELD com/github/ticktakclock/myapplication/User.firstName : Ljava/lang/String; INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; BIPUSH 32 INVOKEVIRTUAL java/lang/StringBuilder.append (C)Ljava/lang/StringBuilder; ALOAD 0 GETFIELD com/github/ticktakclock/myapplication/User.lastName : Ljava/lang/String; INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; ARETURN L1
プロパティ宣言
プロパティ宣言のものについてはフィールド fullName
と getFullName()
メソッドが作成されていて、読みだしたフィールドの値を返却していることがわかります。
fullName
もコンストラクタで文字列の連結が行われて、ゲッターでは計算済みの文字列を返却しています。
カスタムゲッターとメソッドのゲッターは特に違いなし
カスタムゲッターとメソッドのゲッターではフィールドは作られずメソッドだけが作成されていました。
バイトコードちゃんと読めないのですが、中に書かれていることも都度文字列の連結をして返却していることが雰囲気でわかります。
カスタムゲッターとメソッドによるゲッターはどちらも同じ振る舞いをするのですが、Kotlinっぽさが出るのはプロパティ宣言にしてカスタムゲッターを使うやり方かなと思います。(多分好みの問題
ゲッター内のコードの計算量に注意
例えば何度も getFullNameCustom()
を呼び出す場合、プロパティ宣言方式のほうが計算済みの値を返すだけなので効率が良いかもしれないなと思いました。これがfor文とかが入ってO(n)などになるとパフォーマンスに影響するかもですね
まとめ
おそらくKotlinのコードの書き方によって生成されるバイトコードは最も良い形に最適化されて出力されると思うので必ずしもこの通りになることはないかもしれません。
しかし自らが書いたプログラムがどう動こうとしているのかを把握しておくことで「なぜこう書いたのか?」の理由づけや意思決定の材料になったり、困ったときにバイトコードを見てみるといった調査の幅も広げられるのではないかなと思います。