いい加減、何となく動作してるからいいやじゃなくって、中で何やってるのか理解しておくかと、ソースコード読んできたのでまとめておくよ。
InvalidationListenerとChangeListener
ついさっきまで挙動の差とか、全く理解していなかったほど、違いがよく分からんこの二つ。ソースコードを読んでも差が分からなかったので櫻庭さんに解説していただいた。
@nodamushi 値が変更されるとXPropertyBaseのmarkInvalidが2回コールされます。InvalidationListenerだけだと1回目validがtrue、2回目falseになってfireValueChangedEventがコールされないのです。
— Yuichi Sakuraba (@skrb) 2014, 10月 13
@nodamushi 逆にChangeListenerが入っていると、1回目も2回目もvalidがtrueなのでfireValueChangedEventがコールされます。
— Yuichi Sakuraba (@skrb) 2014, 10月 13
@nodamushi InvalidationListnerが登録されている時は、本当にinvalidな時だけ、validが1回目も2回目もtrueになるようです。
— Yuichi Sakuraba (@skrb) 2014, 10月 13
というわけで、どうやらInvalidationListenerとChangeListenerの明確な違いは以下の所にある模様。
//DoublePropertyBaseのmakrInvalidとgetメソッド private void markInvalid() { if (valid) {//←ここと valid = false;//←ここと invalidated(); fireValueChangedEvent(); } } @Override public double get() { valid = true;//←これ return observable == null ? value : observable.get(); }
つまり、getを呼び出さない限り、2回目以降のfireValueChangedEventは呼び出されない。
で、私がInvalidationListenerとChangeListenerの違いが分からなかった理由は次のコード。
//ExpressionHelper.GenericのfireValueChangedEventメソッドの一部抜粋 //InvalidationListenerの呼び出し for (int i = 0; i < curInvalidationSize; i++) { curInvalidationList[i].invalidated(observable); } //ChangeListenerの呼び出し if (curChangeSize > 0) { final T oldValue = currentValue;//currentValueはフィールド変数 //***↓ここでvalidがTrueになる** currentValue = observable.getValue(); final boolean changed = (currentValue == null)? (oldValue != null) : !currentValue.equals(oldValue); if (changed) {//値が変更されたときだけ呼び出す for (int i = 0; i < curChangeSize; i++) { curChangeList[i].changed(observable, oldValue, currentValue); } } }
櫻庭さんの言うChangeListenerを登録しているからInvalidationListenerも呼び出されるというのは、ChangeListenerを呼び出す前にgetを呼び出すからということみたい。
違いが細けぇっ( ゚д゚)
ということは、InvalidationListenerのみの場合でも、毎回getをしてしまえば二つの呼び出しに差がなくなる。テストするときなんかにSystem.out.println(get)なんてやっていると私のように何の違いがあるのかさっぱり分からん!となる。「変化した」というフラグだけ残しておいて、実際に処理するのは描画直前とか、そういう使い方をしたい場合はInvalidListenerの方が効率的という事の模様。
ちなみに、InvalidationListenerもしくはChangeListenerのどちらか一つしかないような場合は、GenericじゃなくてSingle〜っていう名前のクラスで特別に場合分けされている。一つのリスナしか登録しないなんて場面は多々あるから、合理的だね。
ObservableListとInvalidationListenerとListChangeListener
JavaFXで使う機会の多いのがFXCollections.observable(Array)Listかな。observableListでは、渡されたListをラップするOvservableListを返す。従って、渡したリストの中身を変更すると、ObservableListの中身も変わる。作られるObservableListの種類は、Listがランダムアクセスに対応しているかどうかで場合分けされている。
さて、ObservableListの変更がリスナに通知されるのはやっぱり、fireValueChangedEventメソッドだ。ただし、今回はListListenerHelperというクラスのfireValueChangedEventメソッドだが。あと、追加削除等の変更から、実際に通知を行うところまでソースコードを読もうとすると結構長い道のりを辿る。割と面倒くさかったけど、ざっくりというと、配列のいったいどこからどこまでが変更されたのかを記録するクラス(ListChangeBuilder)があって、そいつにここ変更したよとか、終わったよと通知することで、今までの変更を勝手に処理してくれてChangeを作成し、fireValueChangedEventを呼び出してくれる。で、そのfireValueChangedEventの中身を以下に示す。
for (int i = 0; i < curInvalidationSize; i++) { curInvalidationList[i].invalidated(change.getList()); } for (int i = 0; i < curChangeSize; i++) { change.reset(); curChangeList[i].onChanged(change); }
結論としてはListChangeListener使っとけって話ですね。change.reset()は何もしないか、単に内部のカウンタを-1にセットするだけなので重たい処理ではありません。なので、色々情報取得できるListChangeListenerを使っておけば何の問題もないと言うことですね。普通のObservableとどっちも同じリスナで監視したいときなんかはInvalidationListenerの方が良いのかな?
あと、注意点としてはfireValueChangedEventはaddやaddAllを呼び出すたびに実行される。中身のArrayListは十分な容量取ってるから一個一個追加していってもメモリ的に無駄がないはずと思っていても、結構無駄が多い。基本的には、他のArrayListや配列にいったん保存しておいてから、addAllで一発で全部追加するのが効率が良さそうだ。(個人的には、ObjectListenerにbeginChangeとendChageを追加してくれれば良いと思ってる。)
Binding
Bindingって何となく使ってるけど、いったい何で変化を検出してるのか、いつ計算するのか、GCの動きとかどうなってるのかよく理解してないよね。
まず、イベントの検出の仕方はInvalidationListenerを使っている。つまり、値が変化したかどうかは見ていない。さて、気になるメモリ管理だが、このInvalidationListenerではWeakReferenceを用いてインスタンスの保持をしている。つまり、Bindしている状態であっても、GCの動作を阻害しない。
//BindingHelperObserverの一部抜粋 private final WeakReference<Binding<?>> ref; public void invalidated(Observable observable) { final Binding<?> binding = ref.get(); if (binding == null) { observable.removeListener(this); } else { binding.invalidate(); } }
refの中にバインドされている対象が入っている。(=refの内容がobservableの値によって変化する。) バインドされている対象がGCで消されてしまったら、自動的にリスナの登録を解除してくれる。つまり、Bindしたものに関しては基本的に自分でunbindしなくても大丈夫と言うことだね。
で、observableの内容が変化した(可能性がある)場合には、上のソースコードで見て分かるように、invalidateメソッドを呼び出す。これが何をするかというと、変更されたというフラグを立てるだけで、値の再計算は実行されない。実際に値が再計算されるのはgetメソッドを呼び出したときになっている。
//DoubleBindingのgetメソッド @Override public final double get() { if (!valid) {//変化フラグが立っているかどうか value = computeValue();//値の再計算及び保持 valid = true;//フラグを消す } return value; }
実際にgetメソッドを呼び出さない限りはcomputeValueは呼び出されないので、無駄な計算コストを支払うことはない。つまり、いつgetを呼び出すのかということが重要になってくる。変更されるたびに必要もないのにとりあえずgetしていると、無駄な計算コストを支払うことになる。描画直前とか、本当に必要なときだけ呼び出すようにすると良さそう。
JavaFXの何かよく分からんな〜と放置していたブラックボックスがだいぶん見えてきました。やっぱり、中の挙動が分かってる方が、プログラム書いてるときも安心できるよね。