第26週目演習

下準備

テストドライバの導入

第26回目 (秋学期13回目) 分のテストドライバを導入する。以下の手順で行う。

  1. ダウンロード のページを開く (ここをクリック)
  2. プロジェクト「java2007」にある「test」の左側の「+」をクリック
  3. ツリーが展開されるので「install-libraries.xml」を右クリック
  4. 「実行(R)」にマウスカーソルを合わせる
  5. 「1 Ant ビルド」をクリック
  6. 「コンソール」タブに"BUILD SUCCESSFUL"と表示されれば成功
  7. eclipseの画面でプロジェクト「java2007」を右クリック
  8. メニューが表示されるので、「更新」をクリック
  9. "week26.zip" をデスクトップなどにダウンロード
  10. eclipseの画面でプロジェクト「java2007」を右クリック
  11. メニューから「インポート(I)」を選択
  12. 「インポート」ウィンドウが表示されるので、「Zip ファイル」を選択
  13. 「次へ(N)」をクリック
  14. 宛先フォルダー(L): が「java2007」になっていることを確認
  15. From zip file: の右側にある 「参照(R)...」をクリック
  16. ファイルダイアログが表示されるので、ダイアログ内に表示されたダウンロードしたファイルをダブルクリック
  17. 前の画面に戻るので、From zip file: のエリアに正しいパスが入力されていることを確認
  18. フォルダ「/」の左にチェックがついていることを確認 (ついていなければチェックボックスをクリック)
  19. 「警告を出さずに既存リソースを上書き」にチェックがついていることを確認 (上書きしたくないファイルがある場合はチェックを外す)
  20. 「終了 (F)」をクリック

第26週目テストドライバの導入に成功すると、java2007 プロジェクトの test フォルダに j2.lesson13.xml というファイルが作成される。

スレッドや GUI のテスト技法はあまりよいものが確立されていない。今回のテストドライバは試験的に作成したものなので、自動テストに頼らずに各自でテストを行うこと

パッケージの作成

過去の演習を参考にして、「j2.lesson13」というパッケージを作成する。

進捗バーを操作するプログラム

進捗バーはさまざまな場面で使用されているコンポーネントで、例えば Eclipse を起動する際には次のようなバーが表示される。


進捗とは「ものごとの進みぐあい」を表し、上記の例では起動に必要な作業がどの程度進んでいるかを棒グラフのような形で表している。

進捗バーを表示するには、Swing コンポーネントの一つ javax.swing.JProgressBar を使用すると便利である。

package j2.lesson13.example;

import javax.swing.*;

public class ProgressBarSample {

    public static void main(String[] args) {
        JFrame frame = new JFrame("JProgressBar sample");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        
        // 進捗バーを作成
        JProgressBar bar = new JProgressBar();
        
        // 進捗バーの進捗状態を 30% に設定
        bar.setValue(30);
        
        // 進捗バーをウィンドウに追加
        frame.getContentPane().add(bar);
        
        frame.setSize(300, 50);
        frame.setVisible(true);
    }
}

JProgressBar は Swing コンポーネントの一つで、JLabel や JButton と同様に JFrame 上で表示させることができる。また、このクラスは setValue(int) というインスタンスメソッドを持っており、引数に 0~100 の値を指定して呼び出すことにより、進捗バーを百分率で指定した位置まで進めることができる。


今回は、スレッドの操作に慣れるため、この進捗バーをスレッドを用いて操作するようなプログラムを作成する。

このプログラムは、実行すると次のようなウィンドウを表示する。


ウィンドウ下部にある "開始" ボタンを押すと、自動的に少しずつ進捗バーが伸びていく。


進捗バーが自動的に延びていく状態では、ボタンに表示される文字は "開始" から "停止" に変化している。このボタンを押すと、進捗バーが押された状態で停止する。


進捗バーが停止した状態では、ボタンに表示される文字は "停止" から "再開" に変化している。このボタンを押すと、現在の状態から進捗バーが再度自動的に伸び始める。


進捗バーの伸長が再開された状態では、ボタンに表示される文字は "停止" に戻る。このボタンを押すと再度停止する。

進捗バーは、約 0.05 秒ごとに 1% ずつ伸びていくものとする。

設計

複数のスレッドで動作するプログラムは非常に複雑で、同時にいくつかのことを考えてプログラムを書かなければならない場合がある。

擬似コードを上から下に書いただけでは実現できないため、簡単にプログラム全体の見通しを立てておく。

状態の整理

それぞれの状態を整理するため、簡単な図を用意した。


プログラム起動直後の状態は "開始前" に該当し、そこから以下のように遷移していく。

このような状態の遷移を表した図を 状態遷移図 と呼ぶ。今回の描き方は厳密なものではないが、アイデアを整理する際にはこの程度でも便利である。

プログラムの役割分担

プログラムをいくつかの部品に分け、それぞれに役割を分担してみる。

  1. ProgressController
  2. ProgressTask
  3. Swing コンポーネント (すでに用意されている)
  4. ProgressButtonAction

スレッドの役割分担

今回のプログラムは複数のスレッド上で動作させるため、それぞれのスレッド間の関係をまとめる。

これまでは触れなかったが、ウィンドウやそれに関係するコンポーネントは、通常のプログラムとは別のスレッドで動作している。また、main メソッドを実行する際に作成されたスレッドはメインスレッドと呼ばれることが多い。

これらを踏まえて、それぞれのスレッドの役割を図に簡単にまとめてみる。


Swing Thread と書いたスレッドでは、進捗バーが変更された際に再描画したり、ボタンが押された際のイベントを処理したりする。ボタンが押された際には状態を遷移する必要がある。

Main Thread と書いたスレッドは、実はあまり多くのことをしない。ウィンドウを表示した時点でこのスレッドの役割は終了するため、このスレッド上で進捗バーを直接操作することはない。

Progress Thread と書いたスレッドは、進捗バーを定期的に進める状態に遷移するまで作成されない。定期的に進める必要ができたら、一定時間ごとに進捗バーを操作する。

スレッドを役割ごとに分担することにより、それぞれのスレッドは自分の役目だけに専念することができ、結果的に複雑な処理を簡単に書けるようになる。

擬似コードの作成

設計を非常に簡単に済ませたが、次はこの設計を元に擬似コードを作成する。

進捗バーを制御する ProgressController

このプログラムの部品 (後にクラスとして作成する) は、全体を制御するコントローラとして使用する。ここに制御を集中させ、多くの労力を割くことになる。

まずは、プログラム全体を初期化するような擬似コードを作成する。

プログラムを初期化
    状態を "開始前" に変更
    ウィンドウを初期化
    ボタンにアクションを追加

進捗バーを進めるという動作も、制御部分で用意する。

進捗バーを 1 % 進める
    "停止中" の状態なら "進捗中" になるまで待つ
    進捗バーを 1 進める

ボタンが押された際には現在の状態を遷移する。これは制御に類することなので、ここで宣言する。


上図を参考に、擬似コードを作成する。

次の状態へ遷移
    if 現在の状態が "開始前"
        ボタン文字列を "停止" にする
        状態を "進捗中" に変更
        進捗させるスレッドを開始
    if 現在の状態が "進捗中"
        ボタン文字列を "再開" にする
        状態を "停止中" に変更
    if 現在の状態が "停止中"
        ボタン文字列を "停止" にする
        状態を "進捗中" に変更

進捗バーを定期的に進める ProgressTask

このプログラムの部品で行う作業は、「定期的に進捗バーを進める」ということだけである。

進捗バーは 0.05 秒ごとに 0% から 100% まで、1% ずつ進めていくので、以下のような擬似コードで書ける。

定期的に進捗バーを進める
    以下を 100 回繰り返す
        進捗バーを 1 % 進める
        0.05 秒だけ停止
    if 進捗中に割り込みがあった場合
        そのまま終了

ボタンが押された際の挙動を記述する ProgressButtonAction

最後に、ボタンが押された際の処理を擬似コードに変換する。

ボタンが押された際に呼び出される
    次の状態へ遷移する

制御は ProgressController が担っているので、それを呼び出すのみである。

クラスの設計 (ProgressController)

フィールドの抽出

複数の擬似コードに渡って使用されているデータなど、同一擬似コード内で用意できないデータはフィールドとして宣言する。

プログラムを初期化
    状態を "開始前" に変更
    ウィンドウを初期化
    ボタンにアクションを追加
進捗バーを 1 % 進める
    "停止中" の状態なら "進捗中" になるまで待つ
    進捗バーを 1 進める

「進捗バー」はこの擬似コード内で用意できないため、フィールドとして宣言する。

次の状態へ遷移
    if 現在の状態が "開始前"
        ボタン文字列を "停止" にする
        状態を "進捗中" に変更
        進捗させるスレッドを開始
    if 現在の状態が "進捗中"
        ボタン文字列を "再開" にする
        状態を "停止中" に変更
    if 現在の状態が "停止中"
        ボタン文字列を "停止" にする
        状態を "進捗中" に変更

「ボタン」はフィールドとして宣言されている必要がある。

ここまでに「状態」という表現が何度も出てきているが、これもフィールドとして宣言する必要がある。これにはいくつか表し方があるが、ここでは簡単に boolean 型で宣言することにする。


上図より、以下の2つのフィールドが必要になる。

また、終了時にスレッドを強制的に終了させたい場合は、Thread オブジェクトもフィールドに残しておくと便利である。

メソッドの抽出

擬似コードの名前を元に、メソッドを用意する。

また、プログラムの入り口として main メソッドも用意しておく。

クラスの設計 (ProgressTask)

実装するインターフェースの抽出

このプログラムは新しいスレッド上で動作する。そのため、java.lang.Runnable インターフェースを実装する必要がある。

フィールドの抽出

先ほどと同様に、フィールドを抽出する。

定期的に進捗バーを進める
    以下を 100 回繰り返す
        進捗バーを 1 % 進める
        0.05 秒だけ停止
    if 進捗中に割り込みがあった場合
        そのまま終了

このうち、「進捗バーを 1 % 進める」というのは制御を担う ProgressController の役割である。

コンストラクタの抽出

先ほどのフィールド「ProgressController controller」はこのクラス内では用意できない。そのため、コンストラクタの引数に渡してもらう必要がある。

メソッドの抽出

このクラスは java.lang.Runnable を実装するため、実際の仕事は run() メソッド内で行う必要がある。

クラスの設計 (ProgressButtonAction)

実装するインターフェースの抽出

このプログラムは、ボタンが押された際のアクションを記述している。そのため、java.awt.event.ActionListener インターフェースを実装する必要がある。

フィールドの抽出

先ほどと同様に、フィールドを抽出する。

ボタンが押された際に呼び出される
    次の状態へ遷移する

このうち、「次の状態へ遷移する」というのは制御を担う ProgressController の役割である。

コンストラクタの抽出

先ほどのフィールド「ProgressController controller」はこのクラス内では用意できない。そのため、コンストラクタの引数に渡してもらう必要がある。

メソッドの抽出

このクラスは java.awt.event.ActionListener を実装するため、実際の仕事は actionPerformed(ActionEvent e) メソッド内で行う必要がある。

骨格の作成

これまでと同様なので、簡単に済ませる。

ProgressController

package j2.lesson13;

import java.awt.*;
import javax.swing.*;

public class ProgressController {

    private Thread progressThread;
    private JProgressBar bar;
    private JButton button;
    private boolean start;
    private boolean pause;

    // プログラムを初期化
    private void init() {
        // 状態を "開始前" に
        // ウィンドウを初期化
        // ボタンにアクションを追加
    }
    
    // 進捗バーを 1 % 進める
    public synchronized void progress() throws InterruptedException {
        // "停止中" の状態なら "進捗中" になるまで待つ
        // 進捗バーを 1 進める
    }

    // 次の状態へ遷移
    public synchronized void transit() {
        // if 現在の状態が "開始前"
            // ボタン文字列を "停止" にする
            // 状態を "進捗中" に変更
            // 進捗させるスレッドを開始
        // if 現在の状態が "進捗中"
            // ボタン文字列を "再開" にする
            // 状態を "停止中" に変更
        // if 現在の状態が "停止中"
            // ボタン文字列を "停止" にする
            // 状態を "進捗中" に変更
    }
    
    // プログラム全体
    public static void main(String[] args) {
    }
}

ProgressTask

package j2.lesson13;

public class ProgressTask implements Runnable {

    private final ProgressController controller;

    // コンストラクタ
    public ProgressTask(ProgressController controller) {
        this.controller = controller;
    }

    // 定期的に進捗バーを進める
    public void run() {
        // 以下を 100 回繰り返す
            // 進捗バーを 1 % 進める
            // 0.05 秒だけ停止
        // if 進捗中に割り込みがあった場合
            // そのまま終了
    }
}

ProgressButtonAction

package j2.lesson13;

import java.awt.event.*;

public class ProgressButtonAction implements ActionListener {

    private final ProgressController controller;

    // コンストラクタ
    public ProgressButtonAction(ProgressController controller) {
        this.controller = controller;
    }

    // ボタンが押された際に呼び出される
    public void actionPerformed(ActionEvent e) {
        // 次の状態へ遷移する
    }
}

骨格テスト

ここまでの作業をCtrl+Sを押して保存し、コンパイルを行う (保存時に自動で行われる)。ここでエラーが発生していたら文法エラーなので見直す。

「ProgressControllerに対する骨格テスト」を実行する。

骨格テストを行った際に緑のバーが表示されれば、外側から見たプログラムの骨格は正しくなっている。

赤いバーが表示された場合、メッセージを元にプログラムを見直すこと。修正を行い、Ctrl+Sで保存した後に「Run」ボタンをクリックする。

メッセージ 詳細
(クラス名), existence j2.lesson13 に対象のクラスが存在していない。パッケージやクラス名を確認
(メソッド名), existence 指定されたメソッドが存在しない
(メソッド名), public メソッドを作る際に public が抜けている
(メソッド名), protected メソッドを作る際に protected が抜けている
(メソッド名), static メソッドを作る際に static が抜けている
(メソッド名), not static メソッドを作る際に static が余計についている
(メソッド名), synchronized メソッドを作る際に synchronized が抜けている
(メソッド名), type <T> メソッドを作る際に戻り値の型を間違えている (正しくは <T>)
(メソッド名), throws <T> メソッドを作る際に throws の指定がない (正しくは <T>)

プログラムへの変換 (ProgressController.init)

状態を "開始前" に

下図を参考に、擬似コードに即した値をフィールドに設定する。


// 状態を "開始前" に
this.start = false;
this.pause = false;

ウィンドウを初期化

今回は GUI の操作を行うことが主な目的でないので、簡単に済ませる。

// ウィンドウを初期化
JFrame frame = new JFrame("progress bar");
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
this.bar = new JProgressBar();
this.button = new JButton("開始");
frame.getContentPane().add(this.bar, BorderLayout.CENTER);
frame.getContentPane().add(this.button, BorderLayout.SOUTH);
frame.setSize(200, 80);
frame.setVisible(true);

ボタンにアクションを追加

ボタンに追加すべきアクションは、作成中の ProgressButtonAction である。

// ボタンにアクションを追加
this.button.addActionListener(new ProgressButtonAction(this));

プログラムへの変換 (ProgressController.progress)

"停止中" の状態なら "進捗中" になるまで待つ

下図を参考にすると、状態 "停止中" のみが this.pause = true である。


これが false になるまで待ち続ければよいので、下記のように書ける。

// "停止中" の状態なら "進捗中" になるまで待つ
while (this.pause) {
    this.wait();
}

while ではなく if を使用してもこのプログラムならば問題は起きないとは思うが、while にしておいたほうが安全である。notifyAll() はウェイトセット内の全てのスレッドを起こすため、二つ以上のスレッドが同時に wait 状態になっていた場合、if では予想外の動作を行う。

if (this.pause) {
    this.wait();
}

進捗バーを 1 進める

javax.swing.JProgressBar には「1 だけ進める」というメソッドは用意されていない。

代わりに「現在の値を取得する」メソッド getValue() が用意されているので、このメソッドで取得した値を setValue(int) メソッドで設定してやればよい。

// 進捗バーを 1 進める
this.bar.setValue(this.bar.getValue() + 1);

プログラムへの変換 (ProgressController.transit)

この部分は下図を参考にしながら進める必要がある。


if 現在の状態が "開始前"

ボタンが押された際の状態が "開始前" であった場合、図を参考に "進捗中" へと移行する。

ボタン文字列を "停止" にする
状態を "進捗中" に変更

状態が "開始前" であるかどうかを判定するには、

this.start == false && this.pause == false

という条件を満たせばよいが、this.start が false であるような状態は "開始前" しかないため、下記のように簡単に書ける。

// if 現在の状態が "開始前"
if (! this.start) {
    // ボタン文字列を "停止" にする
    this.button.setText("停止");
    // 状態を "進捗中" に変更
    this.start = true;
    this.pause = false;
    
    // 進捗させるスレッドを開始
}

最後の

進捗させるスレッドを開始

の部分では、ProgressTask を新しいスレッドで実行してやればよい。

    // 進捗させるスレッドを開始
    ProgressTask task = new ProgressTask(this);
    this.progressThread = new Thread(task);
    this.progressThread.start();

if 現在の状態が "進捗中"

ボタンが押された際の状態が "進捗中" であった場合、図を参考に "停止中" へと移行する。

ボタン文字列を "再開" にする
状態を "停止中" に変更

状態が "開始前" であるかどうかを判定するには、

this.start == true && this.pause == false

という条件を満たせばよいが、直前の if で this.start の判定をすでに行っているため、下記のように簡単に書ける。

// if 現在の状態が "進捗中"
else if (!this.pause) {
    // ボタン文字列を "再開" にする
    this.button.setText("再開");
    // 状態を "停止中" に変更
    this.start = true;
    this.pause = true;
}

if 現在の状態が "停止中"

ボタンが押された際の状態が "停止中" であった場合、図を参考に "進捗中" へと移行する。

ボタン文字列を "停止" にする
状態を "進捗中" に変更

"開始前" または "進捗中" でないような状態は "停止中" のみである。そのため、単純に else で書ける。

// if 現在の状態が "停止中"
else {
    // ボタン文字列を "停止" にする
    this.button.setText("停止");
    // 状態を "進捗中" に変更
    this.start = true;
    this.pause = false;
    
    this.notifyAll();
}

また、この遷移の最後に「this.notifyAll()」を加えている。これは progress() メソッドが「"停止中" の状態なら "進捗中" になるまで待つ」という部分で this.wait() を呼び出しているため、"停止中" から "進捗中" へと遷移するこの部分では 必ず notifyAll() を呼び出さなければならない。そうしないと、progress メソッド内で呼び出された wait() は停止したままになってしまう。

プログラムへの変換 (ProgressController.main)

main メソッドでは、インスタンスを作成して init() メソッドを呼び出せばよい。

// プログラム全体
public static void main(String[] args) {
    // プログラムを初期化する
    ProgressController controller = new ProgressController();
    controller.init();
}

プログラムへの変換 (ProgressTask.run)

進捗バーを 1 % 進める

このプログラムは新しいスレッド上で動作する。

以下を 100 回繰り返す
    進捗バーを 1 % 進める
    0.05 秒だけ停止

「進捗バーを 1 % 進める」というのは ProgressController の progress() メソッドを呼び出せばよい。

また、「0.05 秒だけ停止」は、Thread.sleep に 50 ミリ秒を指定して停止する。

// 以下を 100 回繰り返す
for (int i = 0; i < 100; i++) {
    // 進捗バーを 1 % 進める
    this.controller.progress();
    
    // 0.05 秒だけ停止
    Thread.sleep(50);
}

上記のままでは、InterruptedException を捕捉できない。これは次の項で説明する。

if 進捗中に割り込みがあった場合

進捗中にこのスレッドに割り込まれた (Thread.interrupt() が呼び出された) 場合、スレッドを終了させる。

if 進捗中に割り込みがあった場合
    そのまま終了

Thread.sleep や Object.wait メソッドを呼び出した際にスレッドが割り込まれた場合、InterruptedException という例外がスローされる。下記の擬似コードは、先ほどのプログラム全体を try で囲って InterruptedException を捕捉してやることで実現できる。

try {
    // 以下を 100 回繰り返す
    for (int i = 0; i < 100; i++) {
        // 進捗バーを 1 % 進める
        this.controller.progress();
        
        // 0.05 秒だけ停止
        Thread.sleep(50);
    }
}
// if 進捗中に割り込みがあった場合
catch (InterruptedException e) {
    // そのまま終了
    return;
}

プログラムへの変換 (ProgressButtonAction.actionPerformed)

次の状態へ遷移する

ProgressController クラス内に定義した同じ名前の擬似コードで実現できる。

// 次の状態へ遷移する
this.controller.transit();

プログラムの実行

一連のプログラムを実行するには、ProgressController クラスの main メソッドを実行すればよい。プログラムを実行すると、下記のようなウィンドウが表示される。


ボタンを操作することにより、さまざまな動作を確認できる。

機能テスト

ここまでの作業をCtrl+Sを押して保存し、コンパイルを行う (保存時に自動で行われる)。ここでエラーが発生していたら文法エラーなので見直す。

「ProgressControllerに対する機能テスト」を実行する。

すると、ProgressController クラスが起動されて、ウィンドウが表示される。しばらく待つとウィンドウが自動で操作され、いくつかのテストケースが実行される (自動で操作されている間はキーボードやマウスを操作しないこと)。

メッセージ 詳細
フレーム <F> が見つかりません <F> の名前を持つフレームが見つからない。JFrame のコンストラクタに指定している文字列を確認
フレーム <F> は javax.swing.JFrame のインスタンスではありません <F> の名前を持つフレームが JFrame のインスタンスでない。java.awt.Frame を誤って使用していないか確認
<機能テストの項目名> <機能テストの項目名> で、FAIL ボタンを押した

機能テストの項目

テストの概要 意味
起動直後の状態 プログラムを起動した直後に表示されるウィンドウの状態
「開始」ボタンを押した直後 「開始」ボタンを押した直後のウィンドウの状態
「開始」ボタンを押した1秒後 上記から 1 秒後のウィンドウの状態
「停止」ボタンを押した直後 上記から「停止」ボタンを押し、その直後のウィンドウの状態
「停止」ボタンを押した0.5秒後 上記から 0.5 秒後のウィンドウの状態
「再開」ボタンを押した0.5秒後 上記から「再開」ボタンを押し、その 0.5 秒後のウィンドウの状態

プログラム全体

ProgressController

package j2.lesson13;

import java.awt.*;
import javax.swing.*;

public class ProgressController {

    private Thread progressThread;
    private JProgressBar bar;
    private JButton button;
    private boolean start;
    private boolean pause;

    // プログラムを初期化
    private void init() {
        // 状態を "開始前" に
        this.start = false;
        this.pause = false;
        
        // ウィンドウを初期化
        JFrame frame = new JFrame("progress bar");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        this.bar = new JProgressBar();
        this.button = new JButton("開始");
        frame.getContentPane().add(this.bar, BorderLayout.CENTER);
        frame.getContentPane().add(this.button, BorderLayout.SOUTH);
        frame.setSize(200, 80);
        frame.setVisible(true);
        
        // ボタンにアクションを追加
        this.button.addActionListener(new ProgressButtonAction(this));
    }
    
    // 進捗バーを 1 % 進める
    public synchronized void progress() throws InterruptedException {
        // "停止中" の状態なら "進捗中" になるまで待つ
        while (this.pause) {
            this.wait();
        }
        // 進捗バーを 1 進める
        this.bar.setValue(this.bar.getValue() + 1);
    }

    // 次の状態へ遷移
    public synchronized void transit() {
        // if 現在の状態が "開始前"
        if (! this.start) {
            // ボタン文字列を "停止" にする
            this.button.setText("停止");
            // 状態を "進捗中" に変更
            this.start = true;
            this.pause = false;
            
            // 進捗させるスレッドを開始
            ProgressTask task = new ProgressTask(this);
            this.progressThread = new Thread(task);
            this.progressThread.start();
        }
        // if 現在の状態が "進捗中"
        else if (this.start && !this.pause) {
            // ボタン文字列を "再開" にする
            this.button.setText("再開");
            // 状態を "停止中" に変更
            this.start = true;
            this.pause = true;
        }
        // if 現在の状態が "停止中"
        else {
            // ボタン文字列を "停止" にする
            this.button.setText("停止");
            // 状態を "進捗中" に変更
            this.start = true;
            this.pause = false;
            
            this.notifyAll();
        }
    }
    
    // プログラム全体
    public static void main(String[] args) {
        // プログラムを初期化する
        ProgressController controller = new ProgressController();
        controller.init();
    }
}

ProgressTask

package j2.lesson13;

public class ProgressTask implements Runnable {

    private final ProgressController controller;

    // コンストラクタ
    public ProgressTask(ProgressController controller) {
        this.controller = controller;
    }

    // 定期的に進捗バーを進める
    public void run() {
        try {
            // 以下を 100 回繰り返す
            for (int i = 0; i < 100; i++) {
                // 進捗バーを 1 % 進める
                this.controller.progress();
                
                // 0.05 秒だけ停止
                Thread.sleep(50);
            }
        }
        // if 進捗中に割り込みがあった場合
        catch (InterruptedException e) {
            // そのまま終了
            return;
        }
    }
}

ProgressButtonAction

package j2.lesson13;

import java.awt.event.*;

public class ProgressButtonAction implements ActionListener {

    private final ProgressController controller;

    // コンストラクタ
    public ProgressButtonAction(ProgressController controller) {
        this.controller = controller;
    }

    // ボタンが押された際に呼び出される
    public void actionPerformed(ActionEvent e) {
        // 次の状態へ遷移する
        this.controller.transit();
    }
}