Java Tips | 日付・時間:タイマー測定

Java Java
スポンサーリンク

「ストップウォッチ」とは何をするクラスか

イメージしてほしいのは、手に持つあのストップウォッチです。
ボタンを押した瞬間に「スタート」、もう一度押したら「ストップ」、表示には「経過時間」が出る。

Java でも同じことをしたくなる場面がたくさんあります。
「この処理、どれくらい時間かかっている?」「外部API呼び出しの時間をログに出したい」「性能テストで処理時間を測りたい」。
そういうときに毎回 start = System.nanoTime() と書くのではなく、「ストップウォッチクラス」にしてしまうと、コードがぐっと読みやすくなります。


なぜストップウォッチは nanoTime を使うのか

currentTimeMillis ではなく nanoTime を使う理由

ストップウォッチの中身は、ほぼ必ず System.nanoTime() を使います。
理由はシンプルで、「経過時間を測るために設計された API」だからです。

System.currentTimeMillis() は「今何時か」を返す“壁時計の時間”で、OS の時刻調整の影響を受けます。
一方 System.nanoTime() は、「ある基準点からの経過ナノ秒」で、単調に増え続けることが保証されています。
ストップウォッチが知りたいのは「今何時か」ではなく「どれくらい時間が経ったか」なので、nanoTime がぴったりです。


最小構成のストップウォッチクラスを作る

startNew と elapsed だけのシンプル版

まずは、最もシンプルなストップウォッチを作ってみます。

import java.time.Duration;

public class Stopwatch {

    private final long startNanos;

    private Stopwatch(long startNanos) {
        this.startNanos = startNanos;
    }

    public static Stopwatch startNew() {
        return new Stopwatch(System.nanoTime());
    }

    public Duration elapsed() {
        long now = System.nanoTime();
        long diff = now - startNanos;
        return Duration.ofNanos(diff);
    }
}
Java

使い方はこうなります。

public class StopwatchExample {

    public static void main(String[] args) {
        Stopwatch sw = Stopwatch.startNew();

        doWork();

        Duration d = sw.elapsed();
        System.out.println("処理時間(ms): " + d.toMillis());
    }

    private static void doWork() {
        try {
            Thread.sleep(400);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
Java

ここで押さえてほしいポイントは三つです。

startNew() で「スタートした瞬間のナノ秒」を記録している。
elapsed() で「今のナノ秒との差」を取り、Duration にして返している。
呼び出し側は「時間の差分計算」を意識せず、「経過時間を教えて」とだけ言えばいい。

「時間の差分計算」をクラスの中に閉じ込めることで、呼び出し側のコードがとても素直になります。


start / stop 型のストップウォッチにする

一度止めてから、経過時間を取りたい場合

「start して、stop して、その時点の時間を固定したい」というケースもよくあります。
その場合は、状態を持つストップウォッチにします。

import java.time.Duration;

public class StatefulStopwatch {

    private long startNanos;
    private long endNanos;
    private boolean running;

    public void start() {
        if (running) {
            throw new IllegalStateException("すでに開始済みです");
        }
        running = true;
        startNanos = System.nanoTime();
        endNanos = 0L;
    }

    public void stop() {
        if (!running) {
            throw new IllegalStateException("開始されていません");
        }
        endNanos = System.nanoTime();
        running = false;
    }

    public Duration elapsed() {
        if (running) {
            long now = System.nanoTime();
            return Duration.ofNanos(now - startNanos);
        } else if (endNanos != 0L) {
            return Duration.ofNanos(endNanos - startNanos);
        } else {
            return Duration.ZERO;
        }
    }
}
Java

使い方はこうです。

public class StatefulStopwatchExample {

    public static void main(String[] args) {
        StatefulStopwatch sw = new StatefulStopwatch();

        sw.start();
        doWork();
        sw.stop();

        System.out.println("処理時間(ms): " + sw.elapsed().toMillis());
    }

    private static void doWork() {
        try {
            Thread.sleep(600);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
Java

ここで深掘りしたいのは、「状態管理」の部分です。

start 済みかどうかを running で管理し、不正な呼び出しには例外を投げる。
stop 済みなら「固定された時間」、まだ動いているなら「今までの経過時間」を返す。

こうしておくと、「二重 start」「stop していないのに elapsed を見ている」といったバグを早期にあぶり出せます。


ラップタイム(区間時間)を測るストップウォッチ

区間ごとの時間を取りたい場合

「全体の処理時間」だけでなく、「このループ1回分」「このAPI呼び出し1回分」など、区間ごとの時間を測りたいこともあります。
そのときは「ラップタイム」を取れるようにします。

import java.time.Duration;

public class LapStopwatch {

    private long startNanos;
    private long lastLapNanos;

    public static LapStopwatch startNew() {
        long now = System.nanoTime();
        LapStopwatch sw = new LapStopwatch();
        sw.startNanos = now;
        sw.lastLapNanos = now;
        return sw;
    }

    public Duration lap() {
        long now = System.nanoTime();
        long diff = now - lastLapNanos;
        lastLapNanos = now;
        return Duration.ofNanos(diff);
    }

    public Duration total() {
        long now = System.nanoTime();
        return Duration.ofNanos(now - startNanos);
    }
}
Java

使い方のイメージです。

public class LapStopwatchExample {

    public static void main(String[] args) {
        LapStopwatch sw = LapStopwatch.startNew();

        doStep("step1");
        System.out.println("step1(ms): " + sw.lap().toMillis());

        doStep("step2");
        System.out.println("step2(ms): " + sw.lap().toMillis());

        doStep("step3");
        System.out.println("step3(ms): " + sw.lap().toMillis());

        System.out.println("total(ms): " + sw.total().toMillis());
    }

    private static void doStep(String name) {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
Java

ここでのポイントは、「ラップを取るたびに基準時刻を更新する」ことです。
lap() を呼ぶたびに「前回 lap からの差分」が返ってくるので、処理のどの部分が重いかを細かく分析できます。


ストップウォッチとログ・セキュリティの関係

「どこが遅いか」を証拠付きで語れるようにする

セキュリティインシデントや障害調査では、「どの処理がどれくらい時間を食っているか」が重要な手がかりになります。
例えば、外部API呼び出しの前後でストップウォッチを使えば、

「このAPIは平均 200ms だが、たまに 5秒かかっている」
「DBクエリは 10ms なのに、アプリ側の処理が 300ms かかっている」

といった事実をログから読み取れます。
これは、性能問題だけでなく、DoS攻撃や外部サービスの異常を検知するうえでも役に立ちます。

測りすぎない、という設計も大事

一方で、あらゆる処理にストップウォッチを入れすぎると、ログが膨れ上がり、逆にシステム全体の性能を落とすこともあります。
実務では、例えば次のような方針を取ります。

重要な処理(外部API、DBアクセス、バッチ処理など)にだけストップウォッチを入れる。
ログレベルや設定で「計測を有効にする範囲」を切り替えられるようにする。
ログフォーマットを統一して、後から集計・可視化しやすくする。

ストップウォッチは「性能とセキュリティのための観測装置」です。
どこをどの粒度で測るかは、設計の一部として意識的に決めていくと良いです。


まとめ:ストップウォッチで身につけてほしい感覚

ストップウォッチは、「処理の前後で時間を取り、その差を Duration として返す」だけのシンプルなユーティリティです。
でも、その中に大事な考え方が詰まっています。

処理時間を測るときは System.nanoTime() を使う。
差分は Duration にして、「時間」として扱う。
startNew / start / stop / lap などのメソッドで、「測りたい意図」をコードに表現する。
どこを測るか、どの粒度でログに出すかを、性能とセキュリティの観点から設計する。

もしあなたのプロジェクトで、「なんとなく遅いけど、どこが遅いのかよく分からない」という状態があるなら、
まずは小さなストップウォッチクラスを一つ作って、気になる処理を挟んでみてください。

数字が出た瞬間に、感覚ではなく事実で話せるようになります。
それが、実務エンジニアとして一段レベルアップする、とても大きな一歩になります。

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