JavaJavaコンソールアプリ

【Java】コンソールで操作するタイマーアプリを作成

Javaタイマーアプリ解説ページのアイキャッチ画像 Java

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タイマーのコア機能マルチスレッディングを実装

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時間表示のフォーマットを実装

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メインアプリケーションロジックユーザーインターフェースを実装

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の解説

クラス定義とインポート

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クラスを定義しています。

フィールド

Java
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: タイマーの実行状態を表す(スレッドセーフ)。

コンストラクタ

Java
public Timer(Duration duration) {
    this.remainingTime = duration;
}
  • タイマーの初期時間を設定します。

start メソッド

Java
public void start() {
    if (isRunning.compareAndSet(false, true)) {
        scheduler.scheduleAtFixedRate(this::tick, 0, 1, TimeUnit.SECONDS);
    }
}
  • タイマーを開始します。
  • compareAndSetを使用して、タイマーが実行中でない場合のみ開始します(スレッドセーフ)。
  • 毎秒tickメソッドを実行するようスケジュールします。

stop メソッド

メソッド定義

Java
public void stop() {
    // ...
}
  • これはstopというパブリックメソッドです。
  • タイマーを停止するための機能を提供します。

タイマーの実行状態チェック

Java
if (isRunning.compareAndSet(true, false)) {
    // ...
}
  • isRunning.compareAndSet(true, false) は重要な操作です。
  • これはアトミック(不可分)な操作で、以下のことを行います:
  1. isRunningtrueかどうかをチェックします。
  2. trueの場合、falseに設定します。
  3. この操作が成功した場合(つまり、タイマーが実行中だった場合)、trueを返します。
  • この方法により、スレッドセーフにタイマーの停止を行えます。

スケジューラーのシャットダウン

Java
scheduler.shutdown();
  • schedulerScheduledExecutorService)のシャットダウンを開始します。
  • これにより、新しいタスクの受け付けが停止されますが、既に実行中のタスクは完了まで実行されます。

シャットダウンの完了待機

Java
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()を呼び出し、強制的にシャットダウンします。

重要なポイント:

  1. スレッドセーフ: compareAndSetを使用して、複数のスレッドから安全に呼び出せるようにしています。
  2. グレースフルシャットダウン: まずshutdown()を呼び出し、実行中のタスクの完了を待ちます。
  3. タイムアウト処理: 1秒間待っても完了しない場合、強制的にシャットダウンします。
  4. 例外処理: 割り込みが発生した場合でも、確実にシャットダウンを実行します。
  5. リソース管理: スケジューラーを適切にシャットダウンすることで、リソースリークを防いでいます。

このメソッドは、並行処理におけるリソースの適切な管理と、様々な状況(正常終了、タイムアウト、割り込み)への対応を示す良い例です。タイマーの停止を安全かつ確実に行うための複雑な処理を実装しています。

tick メソッド

メソッド定義

Java
private void tick() {
    // ...
}
  • これはtickというプライベートメソッドです。
  • タイマーの1秒ごとの動作を定義しています。

残り時間のチェック

Java
if (remainingTime.isZero() || remainingTime.isNegative()) {
    // ...
} else {
    // ...
}
  • remainingTime.isZero(): 残り時間が0かどうかをチェックします。
  • remainingTime.isNegative(): 残り時間が負の値かどうかをチェックします。
  • この条件分岐は、タイマーが終了したかどうかを判断します。

タイマー終了時の処理

Java
stop();
if (onFinish != null) {
    onFinish.run();
}
  • タイマーが終了した場合(残り時間が0以下の場合):
  1. stop()メソッドを呼び出し、タイマーを停止します。
  2. onFinishが設定されている場合(nullでない場合)、それを実行します。
  • onFinishは、タイマー終了時に実行したい処理を定義するためのコールバックです。

タイマー継続時の処理

Java
remainingTime = remainingTime.minusSeconds(1);
if (onTick != null) {
    onTick.run();
}
  • タイマーがまだ終了していない場合:
  1. 残り時間を1秒減らしますminusSeconds(1))。
  2. onTickが設定されている場合(nullでない場合)、それを実行します。
  • onTickは、1秒ごとに実行したい処理を定義するためのコールバックです。

重要なポイント:

  1. 時間管理: Durationクラスを使用して、残り時間を正確に管理しています。
  2. コールバック機能: onFinishonTickを使用することで、タイマーの動作をカスタマイズできます。
  3. null チェック: コールバックを実行する前に、nullでないことを確認しています。これにより、NullPointerExceptionを防いでいます。
  4. メソッドの単一責任: このメソッドは「1秒ごとのタイマーの動作」という単一の責任を持っています。
  5. 条件分岐: タイマーの状態(終了か継続か)に応じて適切な処理を行っています。

このメソッドは、タイマーの核心部分を担っており、時間の管理と適切なアクションの実行を行っています。コールバック機能を使用することで、このタイマークラスの柔軟性と再利用性を高めています。

ユーティリティメソッド

getRemainingTime メソッド

Java
public Duration getRemainingTime() {
    return remainingTime;
}
  • これはゲッターメソッドです。
  • DurationremainingTime(残り時間)を返します。
  • publicなので、クラス外からアクセス可能です。

setOnTick メソッド

Java
public void setOnTick(Runnable onTick) {
    this.onTick = onTick;
}
  • これはセッターメソッドです。
  • RunnableのパラメータonTickを受け取ります。
  • 受け取ったonTickを、クラスのフィールドにセットします。
  • これにより、タイマーの毎秒の動作をカスタマイズできます。

setOnFinish メソッド

Java
public void setOnFinish(Runnable onFinish) {
    this.onFinish = onFinish;
}
  • これもセッターメソッドです。
  • RunnableのパラメータonFinishを受け取ります。
  • 受け取ったonFinishを、クラスのフィールドにセットします。
  • これにより、タイマーの終了時の動作をカスタマイズできます。

isRunning メソッド

Java
public boolean isRunning() {
    return isRunning.get();
}
  • これは状態確認メソッドです。
  • booleanの値を返します。
  • isRunning.get()を呼び出して、タイマーが現在実行中かどうかを返します。
  • isRunningAtomicBoolean型なので、get()メソッドを使用して値を取得します。

重要なポイント:

  1. カプセル化: これらのメソッドは、クラスの内部状態に安全にアクセスする方法を提供しています。
  2. Runnable インターフェース: setOnTicksetOnFinishメソッドはRunnableを使用しており、これはJavaで単一の操作を表すための標準的な方法です。
  3. AtomicBoolean: isRunning()メソッドはAtomicBooleanget()メソッドを使用しており、これはスレッドセーフな方法で真偽値を取得します。
  4. メソッドの命名規則: getsetisというプレフィックスは、それぞれゲッター、セッター、真偽値を返すメソッドであることを示す一般的な規則に従っています。

これらのメソッドは、タイマークラスの公開インターフェースの一部を形成しており、クラスの使用者がタイマーの状態を取得したり、動作をカスタマイズしたりするための手段を提供しています。これにより、タイマークラスの柔軟性と再利用性が向上しています。

TimerDisplay.javaの解説

インポート文

Java
import java.time.Duration;
  • java.time.Durationクラスをインポートしています。
  • このクラスは、時間の長さ(期間)を表現するために使用されます。

クラス定義

Java
public class TimerDisplay {
    // ...
}
  • TimerDisplayというパブリッククラスを定義しています。
  • このクラスは、タイマーの表示に関連する機能を提供します。

formatDuration メソッド

Java
public static String formatDuration(Duration duration) {
    // ...
}
  • これは静的メソッドstatic)です。
  • Duration型のパラメータを受け取り、String型の結果を返します。
  • このメソッドは、時間の長さを整形された文字列に変換します。

時間のフォーマット

Java
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(): 秒部分を取得

重要なポイント:

  1. 静的メソッド: staticキーワードにより、このメソッドはクラスのインスタンスを作成せずに呼び出すことができます。
  2. Duration クラスの使用: Java 8以降で導入されたDurationクラスを使用して、時間の長さを扱っています。
  3. メソッドチェーン: duration.toHoursPart()のように、メソッドを連続して呼び出しています。
  4. 文字列フォーマット: String.format()を使用して、時間を「HH:MM:SS」形式の文字列に変換しています。
  5. ゼロ埋め: %02dを使用することで、各部分が必ず2桁で表示されるようにしています(例:01:05:09)。

このコードは、Durationオブジェクトを人間が読みやすい形式の文字列に変換する便利なユーティリティメソッドを提供しています。タイマーアプリケーションなどで、残り時間を表示する際に使用できます。

TimerApp.javaの解説

インポートと基本構造

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

Java
try (Scanner scanner = new Scanner(System.in)) {
    // ...
}
  • try-with-resources構文を使用してScannerオブジェクトを作成しています。
  • これにより、Scannerは使用後に自動的にクローズされます。

ユーザー入力の処理

Java
System.out.println("タイマーの時間を入力してください。");
System.out.print("時間 分 秒(スペース区切り): ");
Duration duration = Duration.ofHours(scanner.nextLong())
        .plusMinutes(scanner.nextLong())
        .plusSeconds(scanner.nextLong());
scanner.nextLine(); // 改行を消費
  • ユーザーに時間の入力を促します。
  • Durationオブジェクトを作成し、入力された時間、分、秒を設定します。

タイマーの設定

Java
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: タイマー終了時のアクションを設定。

タイマーの開始

Java
System.out.println("タイマーが設定されました: " + TimerDisplay.formatDuration(duration));
System.out.println("タイマー開始はEnterを押してください。");
scanner.nextLine(); // Enterキーの入力を待つ

System.out.println("タイマーを開始しました。");
timer.start();
  • タイマーの設定を表示し、ユーザーの開始指示を待ちます。
  • Enterキーが押されたら、タイマーを開始します。

タイマーの停止

Java
scanner.nextLine(); // Enterキーでの停止を待機
timer.stop();
System.out.println("タイマーが停止しました。");
  • ユーザーがEnterキーを押すのを待ち、タイマーを停止します。

例外処理

Java
} catch (Exception e) {
    System.out.println("エラーが発生しました: " + e.getMessage());
}
  • プログラム実行中に発生する可能性のある例外を捕捉し、エラーメッセージを表示します。

重要なポイント:

  1. ユーザーインターフェース: コンソールベースの対話型インターフェースを提供しています。
  2. Duration クラスの活用: 時間の操作にDurationクラスを使用しています。
  3. ラムダ式: setOnTicksetOnFinishでラムダ式を使用してコールバックを設定しています。
  4. 例外処理: try-catchブロックで潜在的なエラーを処理しています。
  5. リソース管理: try-with-resourcesを使用してScannerを適切に管理しています。

このコードは、コンソールベースのタイマーアプリケーションの実装を示しており、ユーザー入力の処理、タイマーの制御、例外処理など、実践的なJavaプログラミングの多くの側面を含んでいます。

まとめ

  • Timer.javaタイマーのコア機能マルチスレッディングを実装します。
  • TimerDisplay.java時間表示のフォーマットを担当するユーティリティクラスを作成します。
  • TimerApp.javaメインアプリケーションロジックユーザーインターフェースを実装します。
  • スレッドを使用した非同期処理の実装方法
  • try-with-resources文を使用したリソース管理の重要性
  • 例外処理を通じて、堅牢なエラーハンドリングの実装方法

このタイマーアプリケーションの作成を通じて、Javaプログラミングの高度な概念実践的なスキルを身につけることができます。クラス設計スレッド管理ユーザー入力の処理時間操作など、実際のアプリケーション開発で必要となる重要な要素を学ぶことができます。

特に、マルチスレッディングの使用は、Javaの強力な機能を活用した効率的な非同期処理の例を示しています。これにより、リアルタイムの更新スムーズなユーザー体験を実現しています。

また、追加の機能(例:複数タイマーの管理、アラーム機能、GUIインターフェースなど)を実装することで、さらに複雑なアプリケーションへと発展させることができます。

未経験でもWEBスキルを取得したい方
  • Javaコース
    Javaプログラミングを基礎から応用まで、体系的に学べる実践的なカリキュラムが特徴です。「マインクラフト」を通じてJavaの基本文法とフレームワークの概念を学びます。また、実際のプロジェクト開発を通じて、データベース連携やWebアプリケーション開発のスキルを身につけることができます。

コメント

タイトルとURLをコピーしました