今週は,先週に引き続いてSwingでマルチスレッドを扱うためのSwingWorkerクラスを取り上げます。

SwingWorkerクラスはSwingのイベント・ディスパッチ・スレッドと非同期に処理を行うためのクラスです。

利用するには,SwingWorkerクラスを派生させたクラスを作成し,非同期に行う処理とイベント・ディスパッチ・スレッドで実行させる処理を記述します。

別スレッドで非同期に行う処理はdoInBackgroundメソッドに記述します。イベント・ディスパッチ・スレッドで実行させる処理は,実行のタイミングによって二つのメソッドを使い分けます。doInBackgroundメソッドの実行が終了した後に処理させる場合はdoneメソッド,doInBackgroundメソッドとパラレルに実行させる場合はprocessメソッドを使用します。

基本的な使い方

それでは先週示したサンプルを使って,具体的な動作を説明しましょう。

サンプルのソースコード Monologue2.java

先週のサンプルは,フレームにボタンが一つあり,ボタンをクリックすると長い処理が実行されるというものです。長い処理を行っている間は,ボタンの文字列を変更し,使用不可状態にしておきます。

つまり,SwingWorkerクラスを派生させたクラスを作成し,長い処理の部分をdoInBackgroundメソッド,「長い処理が終了した後にボタンの文字列を元に戻し,使用可能にする処理」をdoneメソッドに記述します。

SwingWorkerクラスはジェネリクスでパラメータ化されています。ただ,このサンプルではパラメータは使用しないので,二つのパラメータともObject型にしておきます。

doInBackgroundメソッドとdoneメソッドは親クラスで定義されているので,@Overrideアノテーションを付加しておきます。

// 非同期に行う処理を記述するためのクラス
class LongTaskWorker extends SwingWorker<Object, Object> {
    private JButton button;
    
    public LongTaskWorker(JButton button) {
        this.button = button;
    }
     
    // 非同期に行われる処理
    @Override
    public Object doInBackground() {
        // ながーい処理
        try {
            TimeUnit.SECONDS.sleep(10L);
        } catch (InterruptedException ex) {}
         
        return null;
    }
     
    // 非同期処理後に実行
    @Override
    protected void done() {
        // 処理が終了したので,文字列を元に戻し
        // ボタンを使用可能にする
        button.setText("実行");
        button.setEnabled(true);
    }
}

doInBackgroundメソッド,doneメソッドで記述されているコードは,もともとActionListener#actionPerformedメソッド内に記述していたものです。長い処理はイベント・ディスパッチ・スレッドで実行すると他の処理をブロックしてしまいます。そこで,別スレッドで動作させるため,doInBackgroundメソッドに記述しました。

ボタンの文字列を変更したうえで使用可能にする処理は,長い処理後に行えばいいので,doneメソッドに記述します。

後は,ボタンをクリックされたときのイベント処理を行うactionPerformedメソッドを書き換えます。

button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        // 処理中であることを示すため
        // ボタンの文字列を変更し,使用不可にする
        button.setText("処理中...");
        button.setEnabled(false);
 
        // SwingWorker を生成し,実行する
        LongTaskWorker worker = new LongTaskWorker(button);
        worker.execute();
    }
});

非同期に長い処理を行う前に,ボタンの文字列を変更し,使用不可にします。

SwingWorkerオブジェクトを生成し,SwingWorkerクラスのexecuteメソッドをコールします。

実行すると,ボタンがクリックされていた状態のままだったのが,正しく文字列が変更され,使用不可になります。

SwingWorkerクラスを使ううえでの注意点は,一度使ったSwingWorkerオブジェクトを再び使うことはできないということです。executeメソッドを一度コールした後は,もう一度コールしても何も処理されません。

つまり,非同期に行う何らかのタスクがある場合,そのつどSwingWorkerオブジェクトを生成して,使用するようにします。

二つのパラメータ

ImageViewer
図1 ImageViewer

SwingWokerはジェネリクスの二つのパラメータが使用されていると前述しました。

これらのパラメータは,非同期に実行しているスレッドからイベント・ディスパッチ・スレッドに何らかの情報を引き渡すときに使います。

例えば,非同期にデータベースにアクセスし,その結果をテーブルに表示する場合,データベースのクエリ結果をイベント・ディスパッチ・スレッドに引き渡さなければなりません。

J2SE 5.0より前のJava SEであれば,単にObjectクラスを使用して情報を引き渡し,イベント・ディスパッチ・スレッドで情報をキャストして使用していたはずです。

しかし,これではどうしてもダウンキャストを使わなくてはなりません。そこで,ジェネリクスを使用して,情報の型をパラメータ化したのです。

それでは二つあるパラメータのうち,まず一つ目のパラメータの使い方を見てきましょう。

サンプルのソースコード ImageViewer.java

このサンプルは,クラス名のとおりイメージを表示するだけのアプリケーションです。

フレームの下部にテキスト・フィールドとボタンが二つ表示されています。テキスト・フィールドにはファイル名を記入します。[参照]ボタンをクリックすれば,ファイル・チューザでファイルを選ぶことも可能です。

[ロード]ボタンをクリックすると,イメージ・ファイルを読み込み,フレーム中央部に表示します(図1)。

ここで非同期に行うのは,イメージ・ファイルの読み込みです。

つまり,doInBackgroundメソッドでファイルを読み込み,それをdoneメソッドに引き渡します。

このときに使用する型が一つ目のパラメータになります。イメージの読み込みなのでImageクラスを使用することにしましょう。

class ImageLoadWorker extends SwingWorker<Image, Object> {

コンストラクタでは,ファイル名やイメージを表示するためのラベルなどを設定しておきます。

doInBackgroundメソッドでイメージの読み込みを行います。

@Override
public Image doInBackground() {
    Image image;
    try {
        // イメージの読み込み
        image = ImageIO.read(file);
    } catch (IOException ex) {
        // 例外が発生した場合はnullを返す
        image = null;
    }
     
    return image;
}

doInBackgroundメソッドの戻り値がImageクラスになっているのがわかるはずです。ここがパラメータ化されている部分です。Javadocを見ると,doInBackgroundメソッドの戻り値は,パラメータのTになっています。

ファイルの読み込みは何ら難しいことはありません。Image I/Oを使用してイメージを読み込んでいます。例外が発生した場合にはnullを返すことにしました。

次はdoneメソッドです。

@Override
protected void done() {
    try {
        // doInBackgroundメソッドの戻り値を取得する
        Image image = get();

        if (image != null) {
            // イメージの設定
            imageLabel.setIcon(new ImageIcon(image));
            frame.setTitle(TITLE + " - " + file.getName());
        } else {
            showErrorDialog("イメージがロードできませんでした");
        }
    } catch (InterruptedException ex) {
        showErrorDialog("タイムアウトしました");
    } catch (ExecutionException ex) {
        showErrorDialog("処理が失敗しました");
    }

doInBackgroundメソッドの戻り値は,赤字で示したようにgetメソッドを使用して取得します。getメソッドの戻り値も,doInBackgroundメソッドと同様にパラメータのTになっています。

イメージが取得できれば,それをラベルに設定するだけです。

鋭い方は,この情報のやり取りを見てjava.util.concurrent.Callableインタフェースとjava.util.concurrent.Futureを思いだされたかもしれません。実際,SwingWorkerクラスはConcurrency Utilitiesを使用して実装されています。SwingWorkerクラスのJavadocを見ると,SwingWorkerクラスはFutureインタフェースをインプリメントしていることがわかります。

ところで,イメージを読み込んでいる最中に,またロード・ボタンをクリックされたら困りますね。かといって,ボタンやテキスト・フィールドを一つひとつ使用不可にしていくのも面倒です。

そこで,ここではグラス・ペインを使ってマウス・イベントを扱えないようにしてみました。JFrameクラスは,グラス・ペイン,コンテント・ペインという二つのコンポーネントから構成されています(実際にはもう少し複雑ですが)。

JFrameオブジェクトに貼られるSwingコンポーネントは,コンテント・ペインに貼られます。J2SE 1.4.2まではコンポーネントを追加するときにframe.getContentPane().add(comp)のように書いていたことを覚えておられる方もいるはずです。

グラス・ペインはコンテント・ペインより前面に表示されるコンポーネントです。グラスという名前からもわかるように,透明で通常は表示されません。

グラス・ペインでマウス・イベント処理を行うことにより,コンテント・ペインにマウス・イベントを発生させないようにするのが,ここでの工夫です。

そのために,ImageLoadWorkerクラスのコンストラクタでリスナーを設定します。

public ImageLoadWorker(String fileLocation,
                       JLabel imageLabel,
                       JFrame frame) {
    file = new File(fileLocation);
    this.imageLabel = imageLabel;
    this.frame = frame;

    // マウス・イベントを拾わないようにするためのしかけ
    frame.getGlassPane().addMouseListener(new MouseAdapter() {
        public void mousePressed(MouseEvent event) {
            event.consume();
        }
    });
    frame.getGlassPane().setVisible(true);
}

mousePressedメソッドでイベントを消費(consume)し,下位のコンポーネントにイベントが伝達しないようにします。そして,グラス・ペインを表示します。

これで,doInBackgroundメソッドの処理中はマウスのクリック操作ができないようになります。

このままにしておくとずっとマウスが使えないので,doneメソッドでリスナーを取り外し,グラス・ペインを表示しないようにします。

// マウスイベントを拾えるようにする
frame.getGlassPane().setVisible(false);
MouseListener[] listeners = frame.getGlassPane().getMouseListeners();
for (MouseListener listener: listeners) {
    frame.getGlassPane().removeMouseListener(listener);
}

それでは,もう一つのパラメータに移りましょう。

非同期処理を行っている途中で,イベント・ディスパッチ・スレッドに情報を引き渡したくなることがよくあります。

例えば,プログレス・バーで経過を表示させる場合や,処理が終わった後に,処理が終わった情報の表示を一括ではなく逐次的に更新する場合などです。

このような場合の型を指定するのが,二つ目のパラメータです。

このパラメータは,publishメソッドの引数の型と,processメソッドの引数の型に使用します。

それでは,サンプルで使い方を見ていきましょう。

サンプルのソースコード FileViewer.java

このサンプルは指定されたファイルの内容をテキストエリアに表示するアプリケーションです。ファイルをすべて読み込んだ後に表示するのではなく,表示を逐次更新するようにしています。

そのために,二つ目のパラメータはStringクラスにしました。

class FileLoadWorker extends SwingWorker<Boolean, String> {

もう一つのパラメータは,ファイルの読み込みが成功したかどうかを示すBooleanです。

次に,doInBackgroundメソッドを示します。

@Override
public Boolean doInBackground() {
    // 注 わざとパフォーマンスの悪い書き方をしています
    FileReader reader = null;

    try {
        reader = new FileReader(filename);

        while(true) {
            try {
                java.util.concurrent.TimeUnit.MILLISECONDS.sleep(2);
            } catch (InterruptedException ex) {}

            int c = reader.read();
            if (c == -1) {
                // ファイルを最後まで読めれば成功
                return true;
            }
            
            // 読み込んだ文字をパブリッシュする
            publish(String.valueOf((char)c));
        }
    } catch (IOException ex) {
        // 例外が発生したら不成功
        return false;
    } finally {
        try {
            if (reader != null) {
                reader.close();
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

このファイル読み込みの処理は,逐次処理がわかりやすくなるように,わざと遅い方法で記述してあります。

readメソッドを使用して1文字ずつ読み込みます。その後,読み込んだ文字を引数にpublishメソッドをコールします。publishメソッドによって,イベント・ディスパッチ・スレッドに情報を引き渡すことができます。

情報を受けとるのはprocessメソッドです。

@Override
protected void process(List<String> chunks) {
    // パブリッシュされた文字をテキストエリアに追加
    for (String str: chunks) {
        fileContents.append(str);
    }
}
FileViewer
図2 FileViewer

processメソッドの引数はパラメータのリストになります。publishメソッドがコールされると,すぐさまprocessメソッドがコールされるわけではありません。processメソッドはイベント・ディスパッチ・スレッドの中でスケジューリングされてコールされます。

前回コールされたときから次にコールされるまでの間のpublishメソッドで引き渡された情報が,リストになってprocessメソッドに渡されるのです。

このサンプルではリストに入っているのは,ファイルから読み込んだ文字列なので,それをテキストエリアに追加していきます(図2)。

このようにすることで,非同期に行われる処理とSwingの処理を並列に処理できるようになります。

Swingは,シングルスレッドで実装されている利点と欠点の両方を併せ持っています。Java SE 6では,Swingのシングルスレッドによる欠点がSwingWorkerクラスにより低減されました。

ファイルやデータベースへのアクセス,通信など,Swingのイベント・ディスパッチ・スレッドと非同期に行うべき処理はたくさんあります。つまり,SwingWorkerクラスを使う場面は多いといえるでしょう。

著者紹介 櫻庭祐一

横河電機 ネットワーク開発センタ所属。Java in the Box 主筆

今月の櫻庭

この時期,気になるのは何といってもJavaOne。年に1回,Javaの最大のお祭りです。

今年は5月8日から11日。ゴールデンウィークの直後にサンフランシスコのモスコニセンターで開催されます。

今年はどんなサプライズが飛び出すのでしょうか。Java SE 7はどうなるのか,Java EE 6は。JRubyをはじめとするスクリプトも気になるところ。今から,ワクワクドキドキなのです。