📝

雰囲気で書かないKotlin ~getter編~

2021/06/25に公開4

概要

メガベンチャーの21新卒でAndroidエンジニアをしているほりすです。
Android/Kotlinを書き始めて2年ほど経つのですが、恥ずかしながらgetterの挙動でハマってしまったため、bytecodeをJavaにDecompileし、何が起きているかを調べてみました。

また、以前からLiveDataやStateFlow等で外部に公開する変数をgetterつきで書いている人もいれば、書かない人もいて疑問に思っていました。
具体的には以下のようなコードの違いです。

private val _liveData = MutableLiveData("initial value")
val liveData: LiveData<String> = _liveData

のようにgetterを書かない場合と

private val _liveData = MutableLiveData("initial value")
val liveData: LiveData<String> get() = _liveData

のように書く場合です。

実際にdecompileしてみて、どんな挙動になっているか確かめてみましょう。

結論

  • 初期値としての代入は(当たり前だが)init時に計算され、アクセスされる度に同じインスタンスを返す
  • getterを書くと、アクセスされる度に計算された値が返る

検証

getterを明示的に書かない場合

以下のコードはIntelliJ IDEAで書いています。

class GetterSample {
    private var count = 0
    private val result = "count: $count"

    fun increment() {
        count++
    }

    fun print() {
        println(result)
    }
}

単純に、incrementメソッドでメンバ変数 count の数を増加でき、その結果を別の変数 result にて整形して出力できるprintメソッドがあるだけです。

fun main() {
    val sample = GetterSample()
    sample.print()

    sample.increment()
    sample.print()
}

これを何もせず出力させたのと、incrementした後に出力した時の結果をみます。

output
called 0
called 0

こちらのコードでは当然ですが値は初期値のまま変わりません。

このGetterSample内はどのように展開されているか、Decompileされたコードをみてみましょう

public final class GetterSample {
   private int count;
   private final String result;

   public final void increment() {
      int var10001 = this.count++;
   }

   public final void print() {
      String var1 = this.result;
      boolean var2 = false;
      System.out.println(var1);
   }

   public GetterSample() {
      this.result = "count: " + this.count;
   }
}

ここで大事なのは、コンストラクタでresultの値が計算されているということです。
初期値として値が計算され、それ以降変わることがない(val宣言のため)のです

getterを明示的に書く場合

class GetterSample {
    private var count = 0
    private val result
        get() = "count: $count"
/** 略 */
}

main関数や、その他メソッドは同じなので省きます。
この場合の出力は、

output
count: 0
count: 1

となります。
ここで雰囲気で分かった気にならず、実際にDecompileされたコードを見てどういう挙動になっているかを確認してみましょう。

public final class GetterSample {
   private int count;

   private final String getResult() {
      return "count: " + this.count;
   }
/** 略 */
}

resultがフィールドではなく、getResultというメソッドに変わりました。
つまり、 アクセスされる度にgetResult内の処理を実行し、その結果を返す 挙動になります。

getterとsetter内でバッキングフィールドを使用する(おまけ)

特に変わった挙動はないですが、念のため

class GetterSample {
    private var count = 0
    private var result = "0"
        get() = "counted: $field"
    set(value) {
        field += ", $value"
    }

    fun increment() {
        count++
        result = count.toString()
    }
/** 略 */
}

変更点としては、getterとsetterを書き、バッキングフィールドを使用してみました。

Kotlinの場合は、フィールド(メモリに値を一時的に保存するためのもの)を持つことができないため、初期値を与えることでバッキングフィールドを内部的に自動で作成されます。
getterやsetter内では、無限参照でのStackOverFlowを避けるためにバッキングフィールドを使用します。

出力はこちらです。

output
counted: 0
counted: 0, 1

Decompile後のコードはこちら

public final class GetterSample {
   private int count;
   private String result = "0";

   private final String getResult() {
      return "counted: " + this.result;
   }

   private final void setResult(String value) {
      String var10001 = this.result;
      this.result = var10001 + ", " + value;
   }
/** 略 */
}

フィールドが生成され、getterやsetterで書いた処理はそのフィールドにアクセスする形で実装されています。
なるほど。

結論

再掲です。

  • 初期値としての代入は(当たり前だが)init時に計算され、アクセスされる度に同じインスタンスを返す
  • getterを書くと、アクセスされる度に計算された値が返る
    ということがわかりました。

ちなみに冒頭で言及したLiveDataの件は、 どちらも同じ です。
valで宣言された _liveData は初期値としてMutableLiveDataを代入しており、
liveData ではそれを返しているだけなので、getterを書こうとそのインスタンスは変わりません。

よくよく考えれば当たり前のことですが、bytecodeをJava言語にDecompileして調べてみることで、より深く知ることができました。
Javaからやっているのであればgetterやsetterは書く文化があるので容易に想像ができそうですが、自分のようにKotlin世代だとあまり意識しない点ではあったので、同じような境遇の方は参考になるかも知れません。

Discussion

Moyuru AizawaMoyuru Aizawa

細かいことですが、bytecodeじゃなくて、bytecodeがJavaへdecompileされたコードですね🙏

ho2ri2sho2ri2s

もゆるさん、コメントありがとうございます(先輩だ... 🙏)
少し勘違いしていたので、修正させていただきました!

KamedonKamedon
private val _liveData = MutableLiveData("initial value")
val liveData: LiveData<String> = _liveData

こうしたい目的は、publicでは変更をされたくないからLiveDataのインターフェイスで公開したいだけだと思います。
つまり、、実際にliveDataをobserveするのではなく、中のvalueをobserveするので、同じインスタンスで問題ないと思いますが、どうでしょうか?

ho2ri2sho2ri2s

publicでは変更をされたくないからLiveDataのインターフェイスで公開したい

実際にliveDataをobserveするのではなく、中のvalueをobserveするので、同じインスタンスで問題ない

まさにその通りだと思います!補足ありがとうございます 🙏