「ストップウォッチ」とは何をするクラスか
イメージしてほしいのは、手に持つあのストップウォッチです。
ボタンを押した瞬間に「スタート」、もう一度押したら「ストップ」、表示には「経過時間」が出る。
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 などのメソッドで、「測りたい意図」をコードに表現する。
どこを測るか、どの粒度でログに出すかを、性能とセキュリティの観点から設計する。
もしあなたのプロジェクトで、「なんとなく遅いけど、どこが遅いのかよく分からない」という状態があるなら、
まずは小さなストップウォッチクラスを一つ作って、気になる処理を挟んでみてください。
数字が出た瞬間に、感覚ではなく事実で話せるようになります。
それが、実務エンジニアとして一段レベルアップする、とても大きな一歩になります。
