読者です 読者をやめる 読者になる 読者になる

Sooner or Later

JavaFX

このEntryは JavaFX Advent Calendar の16 日目です。

昨日は @khasunuma さんの JavaFX から Payara Micro を呼び出す際の注意点 でした。

明日は @nodamushi さんです。

そして、この Entry は Java Puzzlers Advent Calendar 2016 の 16 日目でもあります。

昨日は @khasunuma さんの Puzzle : String concatination でした。

そして、明日も @khasunuma さんです。

さて、JavaFX で Puzzle を書くわけですが、GUI は出てきません。

GUI の機能ではないのですが、JavaFX で重要な機能の 1 つにバインドがあります。みなさん、バインド使ってますか?

バインドは JavaFX のプロパティ間で値を自動的に同期させるための機構です。

たとえば、以下のコードではどうでしょう。

    IntegerProperty x = new SimpleIntegerProperty(0);
    IntegerProperty y = new SimpleIntegerProperty();
    y.bind(x);

    x.set(10);
    System.out.println(y.get());

JavaFX ではプロパティを なんちゃらProperty というクラスで表します。この なんちゃらProperty クラスを使うと、バインドももれなく着いてくるというわけです。

で、y は x にバインドしているので、x が変更されると y も勝手に値が更新されます。

なので、このコードを実行すると 10 が出力されます。

バインドは自動で同期してくれるだけではなく、バインドが遅延処理されることも特徴です。

上のコードだと、x が変更されたら、y が変更されるのではありません。y を使用する時に、x が変更されているかどうかをチェックします。この遅延処理によってムダな同期処理を減らすことができるのです。

さて、ここからが Puzzle です。

次のコードを実行したらどうなるでしょう。

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class SoonerOrLater {
    public static void main(String... args) {
        IntegerProperty x = new SimpleIntegerProperty(0);
        IntegerProperty y = new SimpleIntegerProperty();
        y.bind(x);
        
        y.addListener((observable, previous, present) -> {
            System.out.println("Change");
        });
        y.addListener(observable -> {
            System.out.println("Change");
        });

        x.set(10);
        x.set(20);

        System.out.println(y.get());
    }
}

他の Puzzlers Advent の人はちゃんと 4 択にしているんですけど、Java Puzzlers の本はそうでないんですよね。 4 択にするのは、あくまでもプレゼンテーションのためです。

なので、ここでは選択肢は出しません。実行したら、どうなるでしょうか?

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

答えは、次のようになります。

Change
Change
Change
Change
20

たぶん大方の人は、こうなると想像していたと思います。パズルにもなにもなっていないように感じられるかもしれません。

この問題は JavaFX をある程度知っている人が陥るパズルになっているのです。

試しに、次のコードではどうなるでしょう。

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class SoonerOrLater {
    public static void main(String... args) {
        IntegerProperty x = new SimpleIntegerProperty(0);
        IntegerProperty y = new SimpleIntegerProperty();
        y.bind(x);
        
//        y.addListener((observable, previous, present) -> {
//            System.out.println("Change");
//        });
        y.addListener(observable -> {
            System.out.println("Change");
        });

        x.set(10);
        x.set(20);

        System.out.println(y.get());
    }
}

先ほどのパズルのうち、一方の addListener メソッドをコメントアウトしただけです。

実行すると、次のようになります。

Change
20

Change が出力されるのは、2 回だと思いませんでしたか?

ところが、Change が出力されるのは、1 回です。

ラムダ式を使っているので、分かりにくくなっているのですが、x に登録しているリスナはそれぞれインタフェースが異なります。

引数が 3 つのラムダ式が ChangeListener インタフェース、1 つラムダ式が InvalidationListener インタフェースです。

ChangeListener インタフェースは、Java Beans の PropertyChangeListener インタフェースと同様に、プロパティの値が変更されたら、コールされます。

一方の InvalidationListener インタフェースはプロパティが invalid になった時にコールされます。これはどういうことかというと、遅延処理に関係しているわけです。

プロパティの使用時にプロパティの値が valid かどうかを調べて、invalid だったらコールされるのです。

上のコードでは y が使用されるのは、最後の出力の時だけなので、その時に valid かどうかチェックされます。

x は 2 回変更されていますが、valid かどうかのチェックはその後に行うので、結果的に 1 度しかコールされません。

ところが、ChangeListener インタフェースは異なります。バインドされているターゲットの値が変更されてもコールされます。つまり、その都度、値が変化しているかどうかチェックするわけです。

問題は、2 種類のリスナを登録するとどうなるかということです。

バインドのターゲットの値が変更されたら、ChangeListener インタフェースのメソッドがコールされます。この時に、古い値と新しい値を引数に渡します。このプロパティの値を取得するということは、プロパティを使用するということになります。

つまり、valid かどうかのチェックが行われてしまうのです。

ChangeListener インタフェースのメソッドは 2 回コールされるので、InvalidationListener インタフェースのメソッドも 2 回コールされてしまいます。

そして、最後の y.get() 時にも valid かどうかのチェックは行われるのですが、x.set(20) の後に変更はないので、値は valid です。このため、InvalidationListener インタフェースのメソッドはコールされないのです。

このパズルの教訓です。

  • バインドしているプロパティの値をチェックしたいからといって、安易に ChangeListener インタフェースを登録するのはやめよう

Puzzle のネーミング

Joshua Bloch のパズルのネーミングって、とってもウィットに富んでいます。中には日本ではよく分からない習慣や文化に基づいているものもあるので、全部理解できるわけではないのですが...

Java Puzzlers の翻訳をされた柴田さんもパズルのネーミングの意味を調べるのが大変だったらしいですし。

でも、こういうウィットやユーモアを含ませたネーミングというのは、とても重要だと思うわけです。

みんな、パズルを作る時にこういうところまで気にすると、もっとパズルがおもしろくなるのになぁと個人的には思っています。

今回の Sooner or Later ですが、日本語にすると「遅かれ早かれ」ということです。

遅いか早いかはよく分からないけど、最終的にはリスナのメソッドがコールされるよということです。もちろん、遅延処理が絡んでくるからこういうネーミングにしたわけですが、どうでしょうか?