言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

Javaのmethod referenceは初回しか評価されない

はじめに

言いたいことは掲題の通り。method referenceを渡しちゃうと実行時に毎度評価されるわけではない。 毎度評価されたければlambdaを渡した方がいい。

Reference

公式のドキュメントには以下のように記述がある。

Chapter 15. Expressions

The timing of method reference expression evaluation is more complex than that of lambda expressions (§15.27.4). When a method reference expression has an expression (rather than a type) preceding the :: separator, that subexpression is evaluated immediately. The result of evaluation is stored until the method of the corresponding functional interface type is invoked; at that point, the result is used as the target reference for the invocation. This means the expression preceding the :: separator is evaluated only when the program encounters the method reference expression, and is not re-evaluated on subsequent invocations on the functional interface type.

上記の様に最後の一文に「merhod referenceのプログラムに出会った時だけ評価されて、呼び出しのタイミングでは再評価されない」と明言されている。

具体的に

どう言う時に困るかと言うと、例えばクラス生成時に何かをsubscribeしていてviewを更新したいんだけど、viewだけが作り変えられてしまう時とか困る。viewが作り変えられるのでviewのinstanceは新しくなってるんだけど、method referenceでviewを更新しようとしてもこちらは再評価されず古いinstanceを更新しようとしてUIは何も変わらないということが起こりうる。

実際にコード例を示す。

例えばFragmentでBetterKnifeを使っていて、onCreateView() でviewをbindし、

@BindView(R.id.button)
Button button;

@BindView(R.id.image)
MyImageView imageView;

@Nullable
@Override
public View onCreateView(
        @NonNull LayoutInflater inflater,
        @Nullable ViewGroup container,
        @Nullable Bundle savedInstanceState
) {
    View view = inflater.inflate(R.layout.fragment_main, container, false);
    ButterKnife.bind(this, view);
    return view;
}

onViewCreated()本当に初回だけ以下のようなsubscribeを開始するとする。 以下は何かbuttonがあって、押されるたびに別のimage viewのvisibilityがVISIBLE <-> GONEでtoggleになる処理だ。 (何かしらの事情があってviewが作り変えられるたびにsubscribeしたくないものと仮定して読んでほしい :pray: )

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    if (!isInitialized) {
        disposable.add(
                RxView.clicks(button)
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(imageView::toggleVisibility)
        );
    }
    isInitialized = true;
}

一見良さそうに見えるがfragmentが一度detachされ、再度attachされた後、buttonを押してもimage viewのvisibilityは変わらない。この時点でsubscribe()の引数に渡されたmethod referenceは古いview imageのinstanceに対する参照を持っているからだ。

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    if (!isInitialized) {
        disposable.add(
                RxView.clicks(button)
                        .observeOn(AndroidSchedulers.mainThread())
                .subscribe((signal) -> {
                    imageView.toggleVisibility(signal);
                })
        );
    }
    isInitialized = true;
}

こんな感じでlambdaを渡してやることで回避できる。