Javaを使用してコンソールで操作するタイマーアプリケーションを作成する方法を解説します。この記事では、オブジェクト指向プログラミングの原則を活用し、マルチスレッディングを使用した効率的なタイマー機能の実装から、ユーザーインターフェースの構築、そして時間表示のフォーマットを紹介します。
はじめに
この記事のコードをコピペしてEclipseで出力結果を確認してみよう!
コンソールで操作するタイマーアプリ作成
以下のタイマーアプリを作成します。
タイマーの時間を入力してください。
時間 分 秒(スペース区切り): 0 0 5
タイマーが設定されました: 00:00:05
タイマー開始はEnterを押してください。
タイマーを開始しました。
残り時間: 00:00:04 (タイマー停止はEnter)
残り時間: 00:00:03 (タイマー停止はEnter)
残り時間: 00:00:02 (タイマー停止はEnter)
残り時間: 00:00:01 (タイマー停止はEnter)
残り時間: 00:00:00 (タイマー停止はEnter)
タイマーが終了しました!
コード例
- クリックするとコードが表示されます。
Timer.javaでタイマーのコア機能とマルチスレッディングを実装
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* カウントダウンタイマークラス。
* 開始、停止が可能で、ティックごとおよび終了時にコールバックを提供します。
*/
public class Timer {
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private Duration remainingTime;
private Runnable onTick;
private Runnable onFinish;
private final AtomicBoolean isRunning = new AtomicBoolean(false);
/**
* 指定された期間でタイマーを新規作成します。
*
* @param duration タイマーの初期期間
*/
public Timer(Duration duration) {
this.remainingTime = duration;
}
/**
* タイマーが実行中でない場合、タイマーを開始します。
*/
public void start() {
if (isRunning.compareAndSet(false, true)) {
scheduler.scheduleAtFixedRate(this::tick, 0, 1, TimeUnit.SECONDS);
}
}
/**
* タイマーが実行中の場合、タイマーを停止します。
*/
public void stop() {
if (isRunning.compareAndSet(true, false)) {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
}
}
}
/**
* タイマーの各ティックを処理する内部メソッド。
*/
private void tick() {
if (remainingTime.isZero() || remainingTime.isNegative()) {
stop();
if (onFinish != null) {
onFinish.run();
}
} else {
remainingTime = remainingTime.minusSeconds(1);
if (onTick != null) {
onTick.run();
}
}
}
/**
* タイマーの残り時間を取得します。
*
* @return 残り時間(Duration型)
*/
public Duration getRemainingTime() {
return remainingTime;
}
/**
* タイマーの各ティックで実行されるコールバックを設定します。
*
* @param onTick 各ティックで実行されるRunnable
*/
public void setOnTick(Runnable onTick) {
this.onTick = onTick;
}
/**
* タイマーが終了したときに実行されるコールバックを設定します。
*
* @param onFinish タイマー終了時に実行されるRunnable
*/
public void setOnFinish(Runnable onFinish) {
this.onFinish = onFinish;
}
/**
* タイマーが現在実行中かどうかを確認します。
*
* @return タイマーが実行中の場合はtrue、そうでない場合はfalse
*/
public boolean isRunning() {
return isRunning.get();
}
}
TimerDisplay.javaで時間表示のフォーマットを実装
import java.time.Duration;
/**
* タイマーの表示形式を扱うユーティリティクラス。
*/
public class TimerDisplay {
/**
* Duration オブジェクトを "HH:MM:SS" 形式の文字列に変換します。
*
* @param duration 変換する Duration オブジェクト
* @return "HH:MM:SS" 形式でフォーマットされた時間を表す文字列
*/
public static String formatDuration(Duration duration) {
return String.format("%02d:%02d:%02d",
duration.toHoursPart(),
duration.toMinutesPart(),
duration.toSecondsPart());
}
}
TimerApp.javaでメインアプリケーションロジックとユーザーインターフェースを実装
import java.time.Duration;
import java.util.Scanner;
/**
* タイマーアプリケーションのメインクラス。
* ユーザーからの入力を受け取り、タイマーを制御します。
*/
public class TimerApp {
private static Timer timer;
/**
* アプリケーションのエントリーポイント。
* ユーザーからタイマー時間を入力받け、タイマーを開始・停止します。
*
* @param args コマンドライン引数(使用しません)
*/
public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
// タイマー時間の入力を促す
System.out.println("タイマーの時間を入力してください。");
System.out.print("時間 分 秒(スペース区切り): ");
Duration duration = Duration.ofHours(scanner.nextLong())
.plusMinutes(scanner.nextLong())
.plusSeconds(scanner.nextLong());
scanner.nextLine(); // 改行を消費
// タイマーの初期化と設定
timer = new Timer(duration);
timer.setOnTick(() -> {
System.out.println("残り時間: " + TimerDisplay.formatDuration(timer.getRemainingTime()) + " (タイマー停止はEnter)");
});
timer.setOnFinish(() -> {
System.out.println("タイマーが終了しました!");
System.exit(0);
});
// タイマー開始の案内
System.out.println("タイマーが設定されました: " + TimerDisplay.formatDuration(duration));
System.out.println("タイマー開始はEnterを押してください。");
scanner.nextLine(); // Enterキーの入力を待つ
// タイマー開始
System.out.println("タイマーを開始しました。");
timer.start();
// Enterキーでの停止を待機
scanner.nextLine();
timer.stop();
System.out.println("タイマーが停止しました。");
} catch (Exception e) {
System.out.println("エラーが発生しました: " + e.getMessage());
}
}
}
Timer.javaの解説
クラス定義とインポート
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class Timer {
// ...
}
- 必要なクラスをインポートしています。
Timer
クラスを定義しています。
フィールド
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private Duration remainingTime;
private Runnable onTick;
private Runnable onFinish;
private final AtomicBoolean isRunning = new AtomicBoolean(false);
ScheduledExecutorService
: タイマーのタスクを実行するためのスケジューラー。Duration
: 残り時間を表す。Runnable onTick
: 毎秒実行されるタスク。Runnable onFinish
: タイマー終了時に実行されるタスク。AtomicBoolean isRunning
: タイマーの実行状態を表す(スレッドセーフ)。
コンストラクタ
public Timer(Duration duration) {
this.remainingTime = duration;
}
- タイマーの初期時間を設定します。
start メソッド
public void start() {
if (isRunning.compareAndSet(false, true)) {
scheduler.scheduleAtFixedRate(this::tick, 0, 1, TimeUnit.SECONDS);
}
}
- タイマーを開始します。
compareAndSet
を使用して、タイマーが実行中でない場合のみ開始します(スレッドセーフ)。- 毎秒
tick
メソッドを実行するようスケジュールします。
stop メソッド
メソッド定義
public void stop() {
// ...
}
- これは
stop
というパブリックメソッドです。 - タイマーを停止するための機能を提供します。
タイマーの実行状態チェック
if (isRunning.compareAndSet(true, false)) {
// ...
}
isRunning.compareAndSet(true, false)
は重要な操作です。- これはアトミック(不可分)な操作で、以下のことを行います:
isRunning
がtrue
かどうかをチェックします。true
の場合、false
に設定します。- この操作が成功した場合(つまり、タイマーが実行中だった場合)、
true
を返します。
- この方法により、スレッドセーフにタイマーの停止を行えます。
スケジューラーのシャットダウン
scheduler.shutdown();
scheduler
(ScheduledExecutorService
)のシャットダウンを開始します。- これにより、新しいタスクの受け付けが停止されますが、既に実行中のタスクは完了まで実行されます。
シャットダウンの完了待機
try {
if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
}
awaitTermination(1, TimeUnit.SECONDS)
: スケジューラーが1秒以内に全てのタスクを完了するのを待ちます。- もし1秒以内に完了しない場合(
if
文の条件): shutdownNow()
を呼び出し、実行中のタスクを強制的に中断します。InterruptedException
がスローされた場合(待機中に割り込みが発生した場合):catch
ブロック内でshutdownNow()
を呼び出し、強制的にシャットダウンします。
重要なポイント:
- スレッドセーフ:
compareAndSet
を使用して、複数のスレッドから安全に呼び出せるようにしています。 - グレースフルシャットダウン: まず
shutdown()
を呼び出し、実行中のタスクの完了を待ちます。 - タイムアウト処理: 1秒間待っても完了しない場合、強制的にシャットダウンします。
- 例外処理: 割り込みが発生した場合でも、確実にシャットダウンを実行します。
- リソース管理: スケジューラーを適切にシャットダウンすることで、リソースリークを防いでいます。
このメソッドは、並行処理におけるリソースの適切な管理と、様々な状況(正常終了、タイムアウト、割り込み)への対応を示す良い例です。タイマーの停止を安全かつ確実に行うための複雑な処理を実装しています。
tick メソッド
メソッド定義
private void tick() {
// ...
}
- これは
tick
というプライベートメソッドです。 - タイマーの1秒ごとの動作を定義しています。
残り時間のチェック
if (remainingTime.isZero() || remainingTime.isNegative()) {
// ...
} else {
// ...
}
remainingTime.isZero()
: 残り時間が0かどうかをチェックします。remainingTime.isNegative()
: 残り時間が負の値かどうかをチェックします。- この条件分岐は、タイマーが終了したかどうかを判断します。
タイマー終了時の処理
stop();
if (onFinish != null) {
onFinish.run();
}
- タイマーが終了した場合(残り時間が0以下の場合):
stop()
メソッドを呼び出し、タイマーを停止します。onFinish
が設定されている場合(nullでない場合)、それを実行します。
onFinish
は、タイマー終了時に実行したい処理を定義するためのコールバックです。
タイマー継続時の処理
remainingTime = remainingTime.minusSeconds(1);
if (onTick != null) {
onTick.run();
}
- タイマーがまだ終了していない場合:
- 残り時間を1秒減らします(
minusSeconds(1)
)。 onTick
が設定されている場合(nullでない場合)、それを実行します。
onTick
は、1秒ごとに実行したい処理を定義するためのコールバックです。
重要なポイント:
- 時間管理:
Duration
クラスを使用して、残り時間を正確に管理しています。 - コールバック機能:
onFinish
とonTick
を使用することで、タイマーの動作をカスタマイズできます。 - null チェック: コールバックを実行する前に、nullでないことを確認しています。これにより、NullPointerExceptionを防いでいます。
- メソッドの単一責任: このメソッドは「1秒ごとのタイマーの動作」という単一の責任を持っています。
- 条件分岐: タイマーの状態(終了か継続か)に応じて適切な処理を行っています。
このメソッドは、タイマーの核心部分を担っており、時間の管理と適切なアクションの実行を行っています。コールバック機能を使用することで、このタイマークラスの柔軟性と再利用性を高めています。
ユーティリティメソッド
getRemainingTime メソッド
public Duration getRemainingTime() {
return remainingTime;
}
- これはゲッターメソッドです。
Duration
型のremainingTime
(残り時間)を返します。public
なので、クラス外からアクセス可能です。
setOnTick メソッド
public void setOnTick(Runnable onTick) {
this.onTick = onTick;
}
- これはセッターメソッドです。
Runnable
型のパラメータonTick
を受け取ります。- 受け取った
onTick
を、クラスのフィールドにセットします。 - これにより、タイマーの毎秒の動作をカスタマイズできます。
setOnFinish メソッド
public void setOnFinish(Runnable onFinish) {
this.onFinish = onFinish;
}
- これもセッターメソッドです。
Runnable
型のパラメータonFinish
を受け取ります。- 受け取った
onFinish
を、クラスのフィールドにセットします。 - これにより、タイマーの終了時の動作をカスタマイズできます。
isRunning メソッド
public boolean isRunning() {
return isRunning.get();
}
- これは状態確認メソッドです。
boolean
型の値を返します。isRunning.get()
を呼び出して、タイマーが現在実行中かどうかを返します。isRunning
はAtomicBoolean
型なので、get()
メソッドを使用して値を取得します。
重要なポイント:
- カプセル化: これらのメソッドは、クラスの内部状態に安全にアクセスする方法を提供しています。
- Runnable インターフェース:
setOnTick
とsetOnFinish
メソッドはRunnable
を使用しており、これはJavaで単一の操作を表すための標準的な方法です。 - AtomicBoolean:
isRunning()
メソッドはAtomicBoolean
のget()
メソッドを使用しており、これはスレッドセーフな方法で真偽値を取得します。 - メソッドの命名規則:
get
、set
、is
というプレフィックスは、それぞれゲッター、セッター、真偽値を返すメソッドであることを示す一般的な規則に従っています。
これらのメソッドは、タイマークラスの公開インターフェースの一部を形成しており、クラスの使用者がタイマーの状態を取得したり、動作をカスタマイズしたりするための手段を提供しています。これにより、タイマークラスの柔軟性と再利用性が向上しています。
TimerDisplay.javaの解説
インポート文
import java.time.Duration;
java.time.Duration
クラスをインポートしています。- このクラスは、時間の長さ(期間)を表現するために使用されます。
クラス定義
public class TimerDisplay {
// ...
}
TimerDisplay
というパブリッククラスを定義しています。- このクラスは、タイマーの表示に関連する機能を提供します。
formatDuration メソッド
public static String formatDuration(Duration duration) {
// ...
}
- これは静的メソッド(
static
)です。 Duration
型のパラメータを受け取り、String
型の結果を返します。- このメソッドは、時間の長さを整形された文字列に変換します。
時間のフォーマット
return String.format("%02d:%02d:%02d",
duration.toHoursPart(),
duration.toMinutesPart(),
duration.toSecondsPart());
String.format()
メソッドを使用して、時間を整形しています。- フォーマット文字列
"%02d:%02d:%02d"
の意味:%02d
: 2桁の整数(不足する場合は0で埋める):
そのまま出力される区切り文字 duration.toHoursPart()
: 時間部分を取得duration.toMinutesPart()
: 分部分を取得duration.toSecondsPart()
: 秒部分を取得
重要なポイント:
- 静的メソッド:
static
キーワードにより、このメソッドはクラスのインスタンスを作成せずに呼び出すことができます。 - Duration クラスの使用: Java 8以降で導入された
Duration
クラスを使用して、時間の長さを扱っています。 - メソッドチェーン:
duration.toHoursPart()
のように、メソッドを連続して呼び出しています。 - 文字列フォーマット:
String.format()
を使用して、時間を「HH:MM:SS」形式の文字列に変換しています。 - ゼロ埋め:
%02d
を使用することで、各部分が必ず2桁で表示されるようにしています(例:01:05:09)。
このコードは、Duration
オブジェクトを人間が読みやすい形式の文字列に変換する便利なユーティリティメソッドを提供しています。タイマーアプリケーションなどで、残り時間を表示する際に使用できます。
TimerApp.javaの解説
インポートと基本構造
import java.time.Duration;
import java.util.Scanner;
public class TimerApp {
private static Timer timer;
public static void main(String[] args) {
// ...
}
}
- 必要なクラスをインポートしています。
TimerApp
クラスとmain
メソッドを定義しています。static Timer timer
: タイマーオブジェクトを静的フィールドとして宣言しています。
try-with-resources と Scanner
try (Scanner scanner = new Scanner(System.in)) {
// ...
}
- try-with-resources構文を使用して
Scanner
オブジェクトを作成しています。 - これにより、
Scanner
は使用後に自動的にクローズされます。
ユーザー入力の処理
System.out.println("タイマーの時間を入力してください。");
System.out.print("時間 分 秒(スペース区切り): ");
Duration duration = Duration.ofHours(scanner.nextLong())
.plusMinutes(scanner.nextLong())
.plusSeconds(scanner.nextLong());
scanner.nextLine(); // 改行を消費
- ユーザーに時間の入力を促します。
Duration
オブジェクトを作成し、入力された時間、分、秒を設定します。
タイマーの設定
timer = new Timer(duration);
timer.setOnTick(() -> {
System.out.println("残り時間: " + TimerDisplay.formatDuration(timer.getRemainingTime()) + " (タイマー停止はEnter)");
});
timer.setOnFinish(() -> {
System.out.println("タイマーが終了しました!");
System.exit(0);
});
Timer
オブジェクトを作成し、設定します。setOnTick
: 毎秒実行されるアクションを設定(残り時間の表示)。setOnFinish
: タイマー終了時のアクションを設定。
タイマーの開始
System.out.println("タイマーが設定されました: " + TimerDisplay.formatDuration(duration));
System.out.println("タイマー開始はEnterを押してください。");
scanner.nextLine(); // Enterキーの入力を待つ
System.out.println("タイマーを開始しました。");
timer.start();
- タイマーの設定を表示し、ユーザーの開始指示を待ちます。
- Enterキーが押されたら、タイマーを開始します。
タイマーの停止
scanner.nextLine(); // Enterキーでの停止を待機
timer.stop();
System.out.println("タイマーが停止しました。");
- ユーザーがEnterキーを押すのを待ち、タイマーを停止します。
例外処理
} catch (Exception e) {
System.out.println("エラーが発生しました: " + e.getMessage());
}
- プログラム実行中に発生する可能性のある例外を捕捉し、エラーメッセージを表示します。
重要なポイント:
- ユーザーインターフェース: コンソールベースの対話型インターフェースを提供しています。
- Duration クラスの活用: 時間の操作に
Duration
クラスを使用しています。 - ラムダ式:
setOnTick
とsetOnFinish
でラムダ式を使用してコールバックを設定しています。 - 例外処理: try-catchブロックで潜在的なエラーを処理しています。
- リソース管理: try-with-resourcesを使用して
Scanner
を適切に管理しています。
このコードは、コンソールベースのタイマーアプリケーションの実装を示しており、ユーザー入力の処理、タイマーの制御、例外処理など、実践的なJavaプログラミングの多くの側面を含んでいます。
まとめ
- Timer.javaでタイマーのコア機能とマルチスレッディングを実装します。
- TimerDisplay.javaで時間表示のフォーマットを担当するユーティリティクラスを作成します。
- TimerApp.javaでメインアプリケーションロジックとユーザーインターフェースを実装します。
- スレッドを使用した非同期処理の実装方法
- try-with-resources文を使用したリソース管理の重要性
- 例外処理を通じて、堅牢なエラーハンドリングの実装方法
このタイマーアプリケーションの作成を通じて、Javaプログラミングの高度な概念と実践的なスキルを身につけることができます。クラス設計、スレッド管理、ユーザー入力の処理、時間操作など、実際のアプリケーション開発で必要となる重要な要素を学ぶことができます。
特に、マルチスレッディングの使用は、Javaの強力な機能を活用した効率的な非同期処理の例を示しています。これにより、リアルタイムの更新とスムーズなユーザー体験を実現しています。
また、追加の機能(例:複数タイマーの管理、アラーム機能、GUIインターフェースなど)を実装することで、さらに複雑なアプリケーションへと発展させることができます。
コメント