ついにベールを脱いだJavaFX

第11回通信と非同期処理

本連載では、これまでJavaFXのユーザインターフェースに関する部分に関して解説を行ってきました。ここまでくれば、ほとんどのGUIを作り上げることができるはずです。

しかし、ある重要な部分が抜け落ちています。それが今回紹介する通信や非同期処理です。

RIAで特に重要となるのが、HTTPを使用した通信です。多くのアプリケーションはHTTPでサーバとやり取りを行い、情報を送受信します。また、RESTを用いたWebサービスも多く使用されています。

このような通信はローカルのアプリケーションに比べると多大な時間が必要です。そのため、何も考えずに通信を行ってしまうと、アニメーションがストップするなどGUIの応答性の低下を招いてしまいます。そこで、GUIの処理が行われるスレッドとは別のスレッドで通信を行うのが慣例となっています。

JavaFXはAWTやSwingと同じようにイベント駆動でアプリケーションが動作します。通常は、アプリケーションが動作しているスレッドは気にする必要ありませんが、イベントを処理するためのスレッドということでイベントディスパッチスレッドと呼ばれます。通信のように時間のかかる処理は、イベントディスパッチスレッドとは別のスレッドで実行させます。

たとえば、SwingではSwingのイベントディスパッチスレッドと別のスレッドで非同期に処理を行うため、SwingWorkerクラスが提供されています。同じようにJavaFXでも非同期で処理を行う仕組みが用意されています。今回はその仕組みを使って、通信を非同期に行う方法について紹介します。

なお、今回使用したサンプルのソースを含めたNetBeansのプロジェクトは、下記のリンクよりダウンロードすることができます。

HTTPを用いたドキュメントの取得

まずはじめに単純にリモートにあるドキュメントを非同期に取得してみましょう。とはいうものの、非同期であることはほとんど意識することはありません。

プロトコルにHTTPを使用した、ドキュメントの取得にはjavafx.async.RemoteTextDocumentクラスを使用します。

次に簡単なRemoteTextDocumentクラスを使ったサンプルを示します。

リスト1
var document: String;

var remoteText = RemoteTextDocument {
    // ドキュメントのURL
    url: "http://gihyo.jp/dev/serial/01/javafx"
    // HTTPのメソッド
    method: "GET"
    // 取得したドキュメント
    document: bind document with inverse

    // ドキュメントを取得した後にコールされるコールバック関数
    onDone: function(result: Boolean) {
        println("DONE");
        // onDoneがコールされた後、documentにアクセスできる
        println(document);
    }
}
 
Stage {
    title : "Dummy"
    scene: Scene {
        width: 200
        height: 200
        content: [  ]
    }
}

RemoteTextDocumentクラスのurlアトリビュートがリモートにあるドキュメントのURLを表します。ここでは、本連載のインデックスページを指定しました。

同様に、GETやPUTといったHTTPのメソッドはmethodアトリビュートで表されます。現状ではRemoteTextDocumentクラスはプロトコルはHTTPもしくはHTTPS、またメソッドはGETとPOSTに対応しています。methodアトリビュートを指定しない場合、GETが使用されます。そして、取得したドキュメントはdocumentアトリビュートに保持されます。ここでは、青字で示したようにdocument変数にバインドさせています。

RemoteTextDocumentクラスはオブジェクトを生成すると非同期にドキュメントを取得しにいきます。ドキュメントの取得は非同期に行われるため、RemoteTextDocumentクラスのオブジェクト生成は処理がブロックすることはありません。

ドキュメントの取得が完了すると、オレンジで示したonDoneアトリビュートに代入された関数がコールされます。この時点でdocumentアトリビュートに取得した結果が代入されます。

なお、RemoteTextDocumentオブジェクトの動作が非同期で行われるため、そのままではすぐに実行が終了してしまいます。そこで、ダミーのStageオブジェクトを最後に付け足しています。

では、このスクリプトを実行してみましょう。結果を図1に示しました。

図1 RemoteTextDocumentクラスサンプルの実行結果
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<title>騾」霈会シ壹▽縺?↓繝吶?繝ォ繧定┳縺?□JavaFX?徃ihyo.jp 窶ヲ 謚?。楢ゥ戊ォ也、セ</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <<以下、省略>>

なんと、文字化けを起こしてしまいました。これはWindows Vistaで実行したため、ドキュメントがUTF-8で記述されているにもかかわらず、デフォルトの文字セットであるShift_JISで扱ってしまうためです。現状では、RemoteTextDocumentクラスは文字セットを指定することができないので、このようになってしまいます。

そこで、もう1つ通信のために用意されているクラスを使用してみましょう。

HttpRequestクラスを使用した通信

javafx.io.http.HttpRequestクラスはクラス名からもわかるように、HTTPに特化したクラスです。このため、HTTPの細かな制御を行うことも可能です。たとえば、RemoteTextDocumentクラスではGETかPOSTしか使用することができませんでしたが、HttpRequestクラスではPUTやDELETEも扱うことができます。

では、まず先ほどと同じようにリモートにあるドキュメントをHttpRequestクラスを使って取得してみましょう。

リスト2
ar charset: String;
var request: HttpRequest;
 
request = HttpRequest {
    location: "http://gihyo.jp/dev/serial/01/javafx"
 
    onResponseHeaders: function(headerNames: String[]) {
        for (headerName in headerNames) {
            if (headerName == "content-type") {
                // HTTPヘッダの content-type から文字コードを取得
                var contentType
                    = request.getResponseHeaderValue(headerName);
                var index = contentType.indexOf("=");
                charset = contentType.substring(index+1);
            }
        }
    }
 
    onInput: function(stream: InputStream) {
        var reader: BufferedReader;

        if (charset != null) {
            // 文字セットを指定してリーダを生成
            reader = new BufferedReader(
                new InputStreamReader(stream, charset));
        } else {
            reader = new BufferedReader(
                new InputStreamReader(stream));
        }
            
        while(true) {
            var text = reader.readLine();
            if (text == null) {
                break;
            }
            println(text);
        }
        
        stream.close();
    }
}
 
// リクエストをキューに入れる 
request.enqueue();
 
Stage {
    title: "Dummy"
    scene: Scene {
        width: 200
        height: 200
        content: [  ]
    }
}

RemoteTextDocumentクラスを使ったサンプルよりもずいぶん長くなってしまいました。

HttpRequestクラスでは接続するサーバのURLはlocationアトリビュートで設定します。ここでは、先ほどと同じく本連載のインデックスページに設定しました。

HttpRequestクラスではonXというアトリビュートが多く定義されています。X の部分にはConnectingやReadingなどHTTPの状態を表した言葉が入ります。これらのアトリビュートには、個々の状態の時にコールされるコールバック関数を設定することができます。

上記のスクリプトでは、レスポンスのヘッダを読み込んだ後にコールされるonResponseHeadersアトリビュートと、レスポンスのボディを読み込める状態になったときにコールされるonInputアトリビュートを設定しています。

onResponseHeadersアトリビュートでは引数としてヘッダの名前の一覧が与えられます。もし、ここで文字セットを表すcontent-typeがあれば、ヘッダ名から値を取得するgetResponseHeaderValue関数を使用して値を取得します。content-typeは"text/html; charset=UTF-8"のように表されるので、charset=の後の部分を取り出して、charset変数に代入しておきます。

次にonInputアトリビュートでボディの読み込みを行います。onInputアトリビュートでは引数としてInputStreamオブジェクトが与えられます。そこで、先ほど設定したcharsetを使用してリーダを生成させます。

後は、生成したリーダを使用して読み込みを行います。

なお、HttpRequestクラスはオブジェクトを生成するだけでは、通信を行いません。通信を行うのは、青字で示したenqueue関数をコールした後です。

では、実行してみましょう。図2に実行結果を示しました。

図2 HttpRequestクラスサンプルの実行結果
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<title>連載:ついにベールを脱いだJavaFX|gihyo.jp … 技術評論社</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <<以下、省略>>

RemoteTextDocumentクラスを使った場合と異なり、文字化けを起こすことなく読み込みが行えました。

パーサを使って解析

今までのサンプルでドキュメントを取得できることがわかりました。しかし、ドキュメントを取得するだけでは、その情報を活用することができません。

そこで、取得したドキュメントをパースして必要な情報を取り出してみましょう。JavaFXではXMLとJSONをパースできるjava.data.pull.PullParserクラスが提供されています。ここでは、このクラスを使用してFlickrの更新情報を表すAtomをパースし、リストにして表示してみます。

FlickrのAtomはリスト3のようになっています。

リスト3 Flickr Atomの例
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:dc="http://purl.org/dc/elements/1.1/"
      xmlns:flickr="urn:flickr:"
      xmlns:media="http://search.yahoo.com/mrss/">

	<title>Uploads from yuichi.sakuraba</title>
	
        <<省略>>
    
	<entry>
		<title>Entremet Montebello, Pierre Hermé, Nihonbashi Mitsukoshi</title>
		<link rel="alternate" type="text/html" href="http://www.flickr.com/photos/skrb/3224086417/"/>
		<id>tag:flickr.com,2005:/photo/3224086417</id>
		<published>2009-01-25T07:08:47Z</published>
		<updated>2009-01-25T07:08:47Z</updated>
      <dc:date.Taken>2008-12-22T20:39:03-08:00</dc:date.Taken>
		<content type="html"> ... &lt;img src=&quot;http://farm4.static.flickr.com/3531/3224086417_896092041b_m.jpg&quot; ... </content>
        
        <<省略>>
        
        
	</entry>
 
        <<省略>>
 
</feed>

<feed>タグの中に複数の<entry>タグがあり、そこにアップロードされたイメージの情報が記述されています。<content>タグのテキストは長いため一部省略してしまいましたが、htmlでイメージのタイトルなどが表記されており、イメージのURLも含まれています。

ここでは、イメージのタイトルとイメージのURLを抽出して表示するアプリケーションを作成します。アプリケーションの名前はFlickrAtomReaderとしました。

以下にAtomの読み込みとパースを行うAtomReaderクラスのAtom読み込み部分を示します。

リスト4
public class AtomReader {
    public-init var url: String;
    
    public function read(): Void {
        var request = HttpRequest {
            location: url

            // 入力の解析はparse関数で行う
            onInput: parse

            // エラー処理
            onException: function(ex: Exception) {
                println("exception: {ex.getMessage()}");
            }
        };
        request.enqueue();
    }

read関数では、HttpRequestオブジェクトを生成し、読み込みを行うためenqueue関数をコールしています。onInputアトリビュートではparse関数に処理を委譲しています。

では、そのparse関数を見てみましょう。

リスト5
    // 処理が終了した後にコールするコールバック関数
    public var onDone: function(:PhotoInfo[]): Void;
 
    // サムネイルのURLを取り出すための正規表現
    def pattern = java.util.regex.Pattern.compile("http://farm.*_m.jpg");
 
    // Atomのパース
    function parse(stream: InputStream) {
        var infos: PhotoInfo[];
        var info: PhotoInfo;
        var inEntry: Boolean;
 
        var parser = PullParser {
            documentType: PullParser.XML;
            input: stream
            
            onEvent: function(event: Event) {
                if (event.type == PullParser.START_ELEMENT) {
                    if (event.qname.name == "entry") {
                        // <entry>の開始
                        // 新たなPhotoInfoオブジェクトを生成する
                        inEntry = true;
                        info = PhotoInfo {};
                    }
                }
                if (event.type == PullParser.END_ELEMENT) {
                    if (event.qname.name == "entry") {
                        // </entry>の場合
                        // 作成したPhotoInfoオブジェクトをシーケンスに追加
                        inEntry = false;
                        insert info into infos;
                    } else if (inEntry and event.qname.name == "title") {
                        // </title>の場合
                        // PhotoInfoオブジェクトのtitleを設定
                        info.title = event.text;
                    } else if (inEntry and event.qname.name == "content") {
                        // </content>の場合
                        // 正規表現を用いて、サムネイルのURLを取り出し、
                        // PhotoInfoオブジェクトのthumbnailUrlに設定
                        var matcher = pattern.matcher(event.text);
                        if (matcher.find()) {
                            var url = matcher.group();
                            info.thumbnailUrl = url.replaceAll("_m", "_t");
                        }
                    }
                }
            }
        }
        // パースの開始
        parser.parse();
        
        if (onDone != null) {
            // パースが終了したら、コールバック関数をコールする
            onDone(infos);
        }
        
        stream.close();
    }

PullParserクラスはイベント駆動型のパーサで、SAXと同じと考えることができます。

PullServerオブジェクトがパースするドキュメントの種類はdocumentTypeアトリビュートで指定します。ここには、XMLもしくはJSONを指定できます。デフォルトはXMLです。そして、inputアトリビュートに読み込みを行うInputStreamオブジェクトを指定します。

パース時のイベントはonEventアトリビュートに設定した関数で行われます。引数としてjavafx.data.pull.Eventオブジェクトが与えられます。パース処理はEventオブジェクトのtypeアトリビュートによって振り分けます。

ここでは、要素の開始を示すSTART_ELEMENTと要素の終わりを示すEND_ELEMENTを扱いました。START_ELEMENTでは、要素名がentryの場合、新たにPhotoInfoオブジェクトを生成し、entry要素のパース中であることを示すフラグinEntryをtrueにします。PhotoInfoクラスはtitleアトリビュートとthumbnailUrlアトリビュートだけを定義したクラスです。

そして、<title>要素のEND_ELEMENTで、内容をPhotoInfoオブジェクトのtitleアトリビュートに代入します。同じようにthumbnailUrlアトリビュートも設定しますが、<content>要素をそのままthumbnailUrlアトリビュートに代入することはできません。<content>要素で表されるHTMLの中で、イメージのURLはhttp://farmで始まる部分に相当します。そこで、正規表現を用いてこの部分を抽出しています。

ここで取得できるURLはhttp://farm4.static.flickr.com/3531/3224086417_896092041b_m.jpgのように、拡張子の前の部分が_mとなっています。Flickrでは、この部分がイメージのサイズを表しています。

_mの場合イメージの長辺が240ピクセルと少し大きいので、長辺が100ピクセルのイメージを表す_tに変更しています。

<entry>要素のEND_ELEMENTに達したときには、titleアトリビュートもthumbnailUrlアトリビュートも設定されているはずなのでPhotoInfoクラスのシーケンスに作成したPhotoInfoオブジェクトを追加します。

パースが終了したら、コールバック関数であるonDoneをコールします。

では、AtomReaderクラスを使用してサムネイルとタイトルを表示するユーザインターフェースを作成しましょう。

リスト6
// リストで表示する項目
var items: SwingListItem[];
 
var rssReader = AtomReader {
    url: "http://api.flickr.com/services/feeds/photos_public.gne?id=57085156@N00&lang=en-us&format=atom"
    
    onDone: function(infos: PhotoInfo[]) {
        // パースが終了したら、HTMLでリストの項目を表示する
        for (info in infos) {
            var item = SwingListItem {
                text: "<html><p>{info.title}</p><img src='{info.thumbnailUrl}' /></html>"
            }
            insert item into items;
        }
    }
}
rssReader.read();
 
Stage {
    title: "FlickrAtomReader"
    scene: Scene {
        width: 400
        height: 600
        content: [
            // リストをスクロールペインで表示する
            SwingScrollPane {
                width: 400
                height: 600
                
                view: SwingList {
                    items: bind items
                }
            }
        ]
    }
}

ここではjavafx.ext.swing.SwingListクラスを使用して、リストでサムネイルイメージを表示しています。SwingのコンポーネントはHTMLを扱えるため、SwingListItemオブジェクトのtextアトリビュートにタイトルとサムネイルのURLを埋め込んだHTMLを指定しています。

なお、AtomReaderオブジェクトのurlに設定しているのは、筆者のFlickrのAtomです。

実行結果を図3に示します。

図3 FlickrAtomReaderの実行結果

図3 FlickrAtomReaderの実行結果

ここではAtomを題材にしましたが、同じようすることでRESTful Webサービスも扱うことが可能です。

なお、HttpRequestクラスを扱う上で注意すべき点もあります。HttpRequestクラスはコネクト処理などは別スレッドで行うのですが、読み込みを行うonInput関数はイベントディスパッチスレッドで実行します。

このため、onInput関数に時間のかかる処理を記述してしまうと、ユーザインターフェースの反応速度が悪くなってしまいます。JavaFXでは、イベントディスパッチスレッドを意識することがほとんどないので、逆に気をつけないとだめですね。

このようにHttpRequestクラスは、非同期で行う処理が少ないのが残念なところです。より多くの処理を非同期に行うには、JavaFX Desktopに限定されてしまいますが、SwingのSwingWorkerクラスを使用するのがお勧めです。

実をいうと、SwingListItemクラスにイメージを含んだHTMLを表示させると、パフォーマンスが低下します。そこで、SwingListクラスを扱わずにFlickrAtomReaderを作り変えたFlickrAtomViewerを作成してみました。

FlickrAtomViewerではSwingWorkerクラスを使用して非同期にパース処理やサムネイルイメージのロードを行っています。スクリプトの解説は行いませんが、図4にリンクしているアプレットページにスクリプトのソースも示しました。興味のある方はぜひ参考になさってください。

なお、JavaFXはJava SE 6上で動作しているにもかかわらず、SwingWorkerクラスのようにJava SE 6で導入されたクラスは、コンパイルエラーになってしまいます。これはJavaFXがコンパイル時にJ2SE 1.5相当のクラスライブラリに相当するJARファイルを使用しているためです。これを解消するには、2つの方法があります。

  • rt15.jarを入れ替える
  • profileの設定を変更する

rt15.jarというのがJ2SE 1.5のクラスライブラリに相当するファイルです。このファイルはJavaFX SDKであれば、SDKをインストールしたディレクトリの下のlib/desktopディレクトリに存在します。NetBeansを使用している場合は、ユーザディレクトリに存在する.netbeansディレクトリにある6.5/javafx-sdk1.0/lib/desktopディレクトリにあります。ユーザディレクトリはWindows XPの場合、C:\Documents and Settingsディレクトリ、Windows Vistaの場合C:\Usersになります。

はじめの方法は、このファイルを削除してしまい、代わりにJava SE 6のjre/lib/rt.jarファイルをコピーします。そして、rt.jarファイルをrt15.jarとファイル名を変更します。

後者の方法は、rt15.jarを設定している設定ファイルを編集するという方法です。

JavaFX SDKをインストールしたディレクトにprofilesというディレクトリがあります。そのディレクトリの中にあるdesktop.propertiesファイルが設定ファイルです。NetBeansの場合は、ユーザディレクトリの下の.netbeans/6.5/javafx-sdk1.0/profilesディレクトリにあります。

このファイルの中に以下に示したとおりcompile_bootclasspathが設定してあります。

リスト7
compile_bootclasspath="${javafx_home}/lib/shared/javafxc.jar;${javafx_home}/lib/shared/javafxrt.jar;${javafx_home}/lib/desktop/rt15.jar"

この行の最後の${javafx_home}/lib/desktop/rt15.jarの部分を、Java SE 6のrt.jarに置き換えてしまいます。Windowsであれば、${javafx_home}/lib/desktop/rt15.jarをC:/Program Files/jdk1.6.0_12/jre/lib/rt.jarに置き換えます。

これはJava SE 6u12の場合ですが、お使いのJavaのバージョンに応じてディレクトリは変更してください。

図4 FlickrAtomViewerの実行結果

図4 FlickrAtomViewerの実行結果

もう1つの非同期処理用クラス

RemoteTextDocumentクラスもHttpRequestクラスも通信を非同期に行うためのクラスでした。では、通信以外に非同期を行う場合はどうすればよいのでしょう。たとえば、DBへのアクセスや、計算に時間がかかるロジックなどは非同期で行う方が、パフォーマンスの低下を防ぐことができます。

非同期に処理を行うには、2つの方法があります。1つの方法は前節で紹介したSwingWorkerクラスを使用する方法、もう1つの方法がjavafx.async.AbstractAsyncOperationクラスを使用する方法です。ここでは、AbstractAsyncOperationクラスを使用してみましょう。

AbstractAsyncOperationクラスは名前からもわかるようにアブストラクトクラスなので、サブクラスを作成する必要があります。たとえば、RemoteTextDocumentクラスがAbstractAsyncOperationクラスのサブクラスです。

AbstractAsyncOperationクラスは、非同期で処理する部分をJavaで記述する必要があります。このため、ちょっと使いにくくなってしまっています。しかし、ロジック部分をJavaで記述するようなアプリケーションであれば、すでにJavaを使用しているので、すぐに使うことができるはずです。

Java側ではcom.sun.javafx.runtime.async.AbstractAsyncOperationクラスのサブクラスを作成します。JavaFXとJavaではクラス名が同じで、パッケージが異なるだけなので、注意してください。

JavaのAbstractAsyncOperationクラスはConcurrency Utilitiesに含まれるCallableインターフェースを実装しています。しかし、Callableインターフェースが定義しているcallメソッドは実装されていないので、AbstractAsyncOperationクラスのサブクラスがcallメソッドを定義しなくてはなりません。

Javaで非同期処理を行う場合、Runnableインターフェースを実装したクラスを作成するのが一般的ですが、Runnableインターフェースのrunメソッドは戻り値がありません。そのため、非同期で行った処理の結果を返すことが簡単にはできません。一方のCallableインターフェースのcallメソッドは戻り値を持たせることができます。戻り値の型はジェネリクスでパラメータ化されています。

そのため、Callableインターフェースを実装するAbstractAsyncOperationクラスもパラメータ化されています。JavaFXはジェネリクスがないので、ジェネリクスを無視してもいいのですが、コンパイル時に警告が出ます。警告が気になるのであればパラメータを指定した方がいいですね。

あまり意味はないのですが、ここではスレッドを一定期間スリープさせ、スリープから起きた時間をJavaFXのイベントディスパッチスレッドに返すようにしてみました。

リスト8
public class LongLongTaskImpl extends AbstractAsyncOperation<Date> {
    public LongLongTaskImpl(AsyncOperationListener<Date> listener) {
        super(listener);
    }
      
    // 非同期にコールされるメソッド
    @Override
    public Date call() {
        try {
            // 10秒スリープ
            Thread.sleep(10000L);
        } catch (InterruptedException ex) {
            // 非同期処理のキャンセルが行われると、
            // InterruptedException例外が発生する
            // このため、InterruptedException例外が発生した時に
            // 速やかにcallメソッドを抜け出る必要がある
        }

        // 現在時刻を返す
        return new Date();
    }
}

LongLongTaksImplクラスのコンストラクタの引数であるAsyncOperationListenerインターフェースは、非同期の処理が終わった時や、非同期処理のキャンセル時にコールされるコールバックメソッドを定義しているリスナです。しかし、AbstractAsyncOperationクラスが内部的に使用するだけなので、あまり気にする必要はありません。

非同期処理はcallメソッドに記述します。callメソッドの戻り値はクラス定義のジェネリクスパラメータと同一にします。ここでは、java.util.Dateクラスを用いています。

callメソッドでは10秒間スリープした後、現在時刻をDateオブジェクトとして返しています。

重要なのはInterruptedException例外の扱いです。非同期処理がキャンセルされた場合、ブロックしている処理に対してInterruptedException例外が発生します。そのため、InterruptedException例外を適切にキャッチしてcallメソッドを抜けるようにしないと、いつまでたっても処理が終わらいままになってしまいます。

では、次にJavaFX側に移りましょう。

リスト9
public class LongLongTask extends AbstractAsyncOperation {
    // 非同期処理の結果
    public var result: Date;
  
    // 非同期処理を行うJavaクラス
    var task: LongLongTaskImpl;
  
    ublic override function start(): Void {
        // 非同期処理の開始
        task = new LongLongTaskImpl(listener);
        task.start();
    }
  
    public override function cancel(): Void {
        // 非同期処理のキャンセル
        task.cancel();
    }
     
    public override function onCompletion(value: Object): Void {
        // valueがLongLongTaskImpl#callメソッドの戻り値
        result = value as Date
    }
}

JavaFXのAbstractAsyncOperationクラスはstart関数、cancel関数、onCompletion関数の3つの関数をオーバーライドする必要があります。

start関数はJavaのAbstractAsyncOperationクラスのサブクラスを生成して、非同期処理を開始させます。cancel関数はキャンセル処理を記述します。

最後のonCompletion関数は非同期処理が完了した後にコールされるコールバック関数です。onCompletion関数の引数には、JavaのAbstractAsyncOperationクラスのサブクラスのcallメソッドの戻り値が代入されます。JavaFXではジェネリクスはないため、引数の型はObjectクラスになります。

LongLongTaskImplクラスのコンストラクタで指定したAsyncOperationListenerインターフェースは、JavaFXのAbstractAsyncOperationがlistenerアトリビュートとして保持しています。そこで、青字のように、そのままLongLongTaskImplオブジェクトに渡してしまいます。

非同期処理を開始するのはAbstractAsyncOperationクラスのstartメソッドです。そこで、LongLongTaskImplオブジェクトを生成したら、startメソッドをコールします。

キャンセル処理はそのままLongLongTaskImplオブジェクトのcancelメソッドをコールするだけです。onCompletion関数ではcallメソッドの戻り値であるDateオブジェクトをアトリビュートとして保持させています。

このLongLongTaskクラスを使用するスクリプトを以下に示します。

リスト10
var result: Date = new Date();
 
Timeline {
    repeatCount: Timeline.INDEFINITE
    autoReverse: true
    keyFrames: [
        KeyFrame {
            time: 0s
            action: function() {
            	// 非同期処理クラスを生成し、非同期処理を開始する
                def task: LongLongTask;
                task = LongLongTask {
                    // 非同期処理後にコールされる関数
                    onDone: function(flag: Boolean) : Void {
                        result = task.result;
                    }
                }
            }
        },
        KeyFrame {
            time: 5s
        }
    ]
}.play();
 
var ball = Circle { <<省略>> }
 
    <<省略>>
    
Stage {
    title: "Long Long Task"
    scene: Scene {
        width: 300
        height: 200
        content: [ 
            ball,
            Text {
                font : Font {
                    size: 20
                }
                x: 10,
                y: 100
                content: bind "{result}"
            }
        ]
    }
}

ここでは、アニメーションするボールと非同期処理から得られる時間を表示しています。非同期処理はTimelineクラスを使用して5秒ごとに行うことにしました。

AbstractAsyncOperationクラスのサブクラスは生成するだけで自動的にstart関数がコールされます。したがって、明示的にstart関数をコールする必要はありません。

非同期処理クラスを使用するスクリプトが非同期処理が終わった後にコールバック関数を設定したい場合、赤字で示したようにonDoneアトリビュートに記述します。関数の引数は非同期処理が成功したかどうかを示すフラグです。

実行結果を図5に示します。図5はアプレットページにリンクにしていますので、ぜひ実際にアプレットで確かめてみてください。スレッドをブロックしていても、アニメーションはスムーズに行われていることがわかるはずです。

図5 非同期処理の実行結果

図5 非同期処理の実行結果

このようにAbstractAsyncOperationクラスを用いれば、非同期処理の結果を受け渡すことはできました。

しかし、そのままでは途中経過を引き渡すことができません。一方のSwingWorkerクラスでは途中までの情報をpublishメソッドとprocessメソッドのペアで受け渡すことができます図4のFlickrAtomViewerで使用しています⁠⁠。AbstractAsyncOperationクラスでもこのようなデータの受け渡しが欲しいところです。

ここではソースの解説は行いませんが、図6に示したマンデルブロ集合を描画するスクリプトは、キューを用いてスレッド間のデータの受け渡しを行っています。

キューとはリストやマップなどと同じようにデータのコンテナの一種です。キューにはデータを追加するメソッドと、データを取り出すメソッドが定義されています。データは1つずつ追加し、取り出す時も1つずつです。キューの特徴としてデータを取り出す順番が追加された順と同じになります。はじめに追加したデータが、はじめに取り出せるということでFIFO (First In, First Out)と呼ばれます。

キューはイベント駆動のアプリケーションや、複数のスレッドでタスクを受け渡す時によく使用されます。実際に、JavaFXもユーザインターフェースのイベントをキューに入れ、イベントディスパッチスレッドがキューからイベントを取り出して処理をするというアーキテクチャになっています。

アプレットページにソースも示しましたので、参考にしてください。

図6 マンデルブロ集合

図6 マンデルブロ集合

おすすめ記事

記事・ニュース一覧