6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Java(Kotlin/JVM)のラムダ式は必ず別インスタンスとなるわけではない

Posted at

Javaの匿名インナークラスの代わりをラムダ式で書くとき、別インスタンスになると思い込んでいて、インスタンス単位での管理で問題となったメモです。

Android Architecture Components の LiveData を使って、1つの LiveData が管理しているデータソースを別の LiveData としても提供するということを実現するために、空の Observer を LiveData.observer に渡す以下のような実装でハマりました。

サンプルコード

kotlin
// observeされている間だけ1秒おきにインクリメントしていくカウンターLiveData
class Counter : LiveData<Int>() {
    private var timer: Timer? = null

    // アウタークラスのカウンターに依存しつつ、偶数だけを配信するLiveData
    var oddCounter: MutableLiveData<Int> = object : MutableLiveData<Int>() {
        override fun observe(owner: LifecycleOwner, observer: Observer<Int>) {
            super.observe(owner, observer)

            // アウタークラスにカウンターの実体があるため、
            // そちらも observe することで active にし、カウンターを開始する
            this@Counter.observe(owner, Observer<Int> { })
        }
    }

    override fun onActive() {
        val task = object : TimerTask() {
            override fun run() {
                var nextCount = (value ?: 0) + 1

                postValue(nextCount)
                if (nextCount % 2 != 0) {
                    oddCounter.postValue(nextCount)
                }
            }
        }
        timer = Timer()
        timer?.scheduleAtFixedRate(task, 0, 1000)
    }

    override fun onInactive() {
        timer?.cancel()
    }
}

class MainKotlinActivity : AppCompatActivity() {
    private val counter = Counter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 複数のownerがobserveする実装のサンプルで、アクティビティとプロセスがobserveするコード
        counter.oddCounter.observe(this, Observer<Int> { value ->
            value?.let { println("activity got $it") }
        })
        counter.oddCounter.observe(ProcessLifecycleOwner.get(), Observer<Int> { value ->
            value?.let { println("process got $it") }
        })
    }
}

これを実行すると、 this@Counter.observe(owner, Observer<Int> {}) というコードが、異なる owner インスタンスに対して2回呼び出されますが、2回目で java.lang.IllegalArgumentException: Cannot add the same observer with different lifecycles という例外が発生します。

原因は、次の通り。

おそらく、 Observer<Int> { } だとクロージャが呼び出し元のスコープに依存しておらず固定であるため、1つのインスタンスを流用しているんだろう。
実際、 Observer<Int> { }Observer<Int> { print(owner.toString()) } とする、つまり呼び出しのたびに変わる owner に依存する実装になっていれば、別インスタンスを返しました。

そもそもバイトコードとしては匿名インナークラスとラムダ式だと全く異なるんだろうと思い、 http://www.ne.jp/asahi/hishidama/home/tech/java/lambda.html#h_invokedynamic このへんを読んでフムフム。


今回、空実装のObserverインスタンスを作りたくて Observer<Int> { } と書いてしまっていたわけですが、IDE の出す warning に従ったらそうなってしまったという背景があります。実際、以下のようにいくつかの方法で書けるわけですが。。

もっともオーソドックスなやり方: Java の匿名インナークラス。

java
Observer<Integer> nullObserver = new Observer<Integer>() {
    @Override
    public void onChanged(@Nullable Integer integer) {
    }
};
observe(owner, nullObserver);

でもこれは、Android Studio に「can be replaced with lambda」と提案され、それに従うと以下のようにラムダ式を使うコードに変換されます。

java
Observer<Integer> nullObserver = value -> {};
observe(owner, nullObserver);

Kotlinでも同様。

kotlin
val nullObserver = object : Observer<Int> {
    override fun onChanged(t: Int?) {
    }
}
observe(owner, nullObserver);
kotlin
val nullObserver = Observer<Int> { }
observe(owner, nullObserver);

それぞれの違いを意識して使い分けないといかんなと、再認識しました。

単にもとの匿名インナークラスやオブジェクト式のままにしておい場合、別の人がIDEの提案に従って再度同じ変更をしてしまう可能性もあるため、以下のように専用のクラスをインスタンス化するようにしました。

kotlin
private class NullObserver<T> : Observer<T> {
    override fun onChanged(t: T?) {
    }
}

observe(owner, NullObserver<Int>())

いまqiita記事をまとめながら、ソースコードコメントでなにか補足しておいても良かったのかなとも思いました。

おわり。


ちなみに、ここに書いたサンプルコードだと Transformations を使えばこんなややこしい空observeみたいなことをしなくてすむ気もしますが、実際のコードはもう少し込み入った事情があったりはします。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?