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

Java Java
スポンサーリンク

「タイマー測定」で何をしたいのかイメージする

業務システムを書いていると、
「この処理、どれくらい時間かかっているんだろう?」
「リリース前に性能をざっくり測りたい」
「ログに処理時間を出しておきたい」
という場面が必ず出てきます。

ここで使うのが「タイマー測定」です。
Java では主に System.currentTimeMillis()System.nanoTime()、そして Duration を組み合わせて、処理時間を測ります。
ただし、この2つは“性質”が違うので、そこを理解して使い分けることがとても大事です。


currentTimeMillis と nanoTime の違いを理解する

currentTimeMillis は「壁時計の時刻」

System.currentTimeMillis() は、
「1970-01-01T00:00:00Z からの経過ミリ秒(UTC基準)」を返します。

public class CurrentTimeMillisSample {

    public static void main(String[] args) {
        long now = System.currentTimeMillis();
        System.out.println("currentTimeMillis: " + now);
    }
}
Java

これは「今何時か?」を知るには便利ですが、
OS の時刻調整(NTP など)で時間が戻ったり進んだりする可能性があります。
そのため、「正確な経過時間を測る」用途には向きません。

nanoTime は「経過時間を測るためのカウンタ」

System.nanoTime() は、
「ある任意の基準点からの経過ナノ秒」を返します。

public class NanoTimeSample {

    public static void main(String[] args) {
        long t = System.nanoTime();
        System.out.println("nanoTime: " + t);
    }
}
Java

この値は「現在時刻」ではありませんが、
単調に増加し続けることが保証されているので、
end - start のように差を取ることで、経過時間を正確に測るのに向いています。

ここが超重要ポイントです。
時刻を知りたいなら currentTimeMillis
処理時間を測りたいなら nanoTime
この役割の違いを体に染み込ませておくと、性能計測の設計が一気に安定します。


一番基本的なタイマー測定パターン

nanoTime で処理時間を測る

まずは、最もシンプルな「タイマー測定」の例です。

public class SimpleTimer {

    public static void main(String[] args) {
        long start = System.nanoTime();

        doWork();

        long end = System.nanoTime();

        long elapsedNanos = end - start;
        double elapsedMillis = elapsedNanos / 1_000_000.0;

        System.out.println("処理時間(ナノ秒): " + elapsedNanos);
        System.out.println("処理時間(ミリ秒): " + elapsedMillis);
    }

    private static void doWork() {
        try {
            Thread.sleep(500); // 0.5秒だけ待つ
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
Java

流れはとてもシンプルです。

開始前に start = System.nanoTime() を取る。
処理が終わったら end = System.nanoTime() を取る。
end - start が「経過ナノ秒」。必要に応じてミリ秒や秒に変換する。

ここで意識してほしいのは、「測りたい範囲を、start と end で挟む」という感覚です。
この“挟む”感覚が身につくと、どんな処理でもタイマー測定できるようになります。


Duration を使って「時間らしく」扱う

nanoTime の差を Duration に変換する

生の long 値のままでもいいのですが、
「時間」として扱うなら Duration を使うと読みやすくなります。

import java.time.Duration;

public class TimerWithDuration {

    public static void main(String[] args) {
        long start = System.nanoTime();

        doWork();

        long end = System.nanoTime();

        long elapsedNanos = end - start;
        Duration duration = Duration.ofNanos(elapsedNanos);

        System.out.println("Duration      : " + duration);
        System.out.println("ミリ秒        : " + duration.toMillis());
        System.out.println("秒(小数あり)  : " + duration.toNanos() / 1_000_000_000.0);
    }

    private static void doWork() {
        try {
            Thread.sleep(750); // 0.75秒
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
Java

Duration を使うと、
「ミリ秒」「秒」「分」などへの変換がメソッドで書けるので、
コードの意図がとても読みやすくなります。

「処理時間は Duration で持つ」という癖をつけておくと、
後から「ログにはミリ秒で出したい」「メトリクスには秒で送りたい」といった要件が出ても柔軟に対応できます。


ログに処理時間を出す実務的パターン

try-finally で「測り忘れ」を防ぐ

業務コードでは、「例外が出ても処理時間をログに出したい」ということがよくあります。
そのときは try-finally でタイマーを挟むのが定番です。

import java.time.Duration;

public class LoggingTimer {

    public static void main(String[] args) {
        long start = System.nanoTime();
        try {
            doBusinessLogic();
        } finally {
            long end = System.nanoTime();
            Duration d = Duration.ofNanos(end - start);
            System.out.println("[LOG] doBusinessLogic 処理時間(ms): " + d.toMillis());
        }
    }

    private static void doBusinessLogic() {
        // ここに業務処理を書く
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
Java

ポイントは、「どんな終了パスでも finally が必ず実行される」ということです。
例外が投げられても、処理時間は必ずログに残ります。

このパターンをユーティリティ化しておくと、
「この処理、ちょっと遅い気がするな」と思ったときに、すぐ計測を仕込めるようになります。


小さなタイマー用ユーティリティを作る

シンプルな Stopwatch クラス

毎回 startend を書くのが面倒なら、
小さな「ストップウォッチ」クラスを作ってしまうのも実務ではよくやる手です。

import java.time.Duration;

public class Stopwatch {

    private long startNanos;

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

    public Duration elapsed() {
        long now = System.nanoTime();
        return Duration.ofNanos(now - startNanos);
    }
}
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

「タイマー測定したい」と思ったら、
Stopwatch sw = Stopwatch.startNew();
と書いておいて、終わったところで sw.elapsed() を呼ぶだけです。

こういう小さなユーティリティを自分で作っておくと、
プロジェクト全体で同じ書き方になり、読みやすさもぐっと上がります。


セキュリティ・性能観点から見たタイマー測定

性能問題の“証拠”を残す

セキュリティインシデントや障害調査では、
「どの処理がどれくらい時間を食っているか」が重要な手がかりになります。

例えば、外部API呼び出しの前後でタイマー測定をしておけば、
「このAPIは平均 200ms だが、たまに 5秒かかっている」といった事実をログから読み取れます。
これは、性能問題やDoS攻撃の兆候を見つけるうえでも役に立ちます。

タイマー測定自体が“重くなりすぎない”ようにする

一方で、あらゆる処理にタイマー測定を入れすぎると、
ログが膨れ上がり、逆にシステム全体の性能を落とすこともあります。

実務では、例えば次のような考え方をします。

普段は重要な箇所だけタイマー測定を有効にする。
詳細な計測が必要なときだけ、設定で「計測レベル」を上げる。
ログ出力は必要最低限のフォーマットにして、解析しやすくする。

「どこを、どの粒度で測るか」は、性能とセキュリティの両方を意識した設計の一部だと考えてください。


まとめ:タイマー測定で身につけてほしい感覚

タイマー測定は、「処理の前後で時間を取って、その差を計算する」だけのシンプルな仕組みです。
でも、その中に大事なポイントがいくつも隠れています。

処理時間を測るときは System.nanoTime() を使う。
差をそのまま long で持つのではなく、Duration にして「時間」として扱う。
try-finally や小さな Stopwatch ユーティリティで、「測り忘れ」「書き忘れ」を防ぐ。
どこを測るか、どの粒度でログに出すかは、性能とセキュリティを意識して設計する。

もしあなたのプロジェクトで、
「なんとなく遅いけど、どこが遅いのかよく分からない」という状態があるなら、
まずは小さなタイマー測定を一箇所入れてみてください。

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

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