デバッグの全体像
デバッグは「バグの原因を特定して、意図どおりに直す」作業です。Javaでは、再現手順の固定、ログやスタックトレースの読み取り、IDEのブレークポイントでの実行停止・変数観察、最小化したテストケースの切り出しが基本になります。重要なのは「思い込みを捨てて、事実を観測する」こと。症状の記録→再現→観測→仮説→検証→修正→再テストの流れを短く回すのがコツです。
まずやること:再現と事実の記録
再現条件を固定する
- 入力と環境を固定: 同じ引数・ファイル・環境変数・時刻で再現できる状態にします。
- 最小ケースへ縮小: 問題が発生する最小限のコードやデータに削ると、原因が浮き上がります。
// まずは「この1行」で落ちるかを再現
System.out.println(Integer.parseInt("123")); // OK
System.out.println(Integer.parseInt("12a")); // ここで NumberFormatException
Java症状を正確に書き出す
- 例外メッセージ: 「何が・どこで」出たかをそのまま記録(コピペ推奨)。
- スタックトレース: 先頭の原因行(クラス名:行番号)から読み始める。
Exception in thread "main" java.lang.NumberFormatException: For input string: "12a"
at Sample.main(Sample.java:5)
Javaログとスタックトレースの読み方(重要ポイントの深掘り)
スタックトレースの要点
- 先頭行が出発点: 「どの変数が null」「0で除算」など最近のJDKはヒント付き。該当行のコードを開いて、どの参照・値が問題かを特定します。
- 原因連鎖(cause)を辿る: 「包み直し」がある場合は Caused by の最後まで読むと根本原因に到達します。
ログは「事実」を残す
- 何が・いつ・どの入力で: ID・入力値・ステータスをキー=値で記録。
- 一度だけ出す: 下位でログ→上位でもログを重複させない。分析が難しくなります。
log.error("read_failed path={} reason={}", path, e.toString());
JavaIDE デバッグ:ブレークポイントとウォッチ
実行を止めて見る
- ブレークポイント: 該当行で停止。条件付き(変数が特定値の時だけ止める)を使うと効率的。
- ステップ操作: ステップイン(メソッド内部へ)、ステップオーバー(次の行へ)、ステップアウト(呼び出し元へ戻る)で流れを追う。
変数の中身を観測する
- ウォッチ: 監視したい式(例:list.size(), map.get(key))を登録して、行ごとに値を確認。
- 評価: IDEの「Evaluate」機能で、その場で式を評価すると仮説検証が速い。
// ブレークポイントで停止中に list.size() をウォッチして「期待通りか」を確認
Java小さく切り出す:最小テストケースと再現コード
問題だけを抽出する
- 独立した main や JUnit: 問題のある処理だけを呼ぶミニプログラムを作る。
- 外部依存を切る: ダミー入力・スタブ化で環境差を排除。ロジック単体を検証。
public class Repro {
public static void main(String[] args) {
System.out.println(slice("abcdef", 2, 5)); // 期待:"cde"
System.out.println(slice("abc", 2, 5)); // 例外で再現
}
static String slice(String s, int begin, int end) {
if (begin < 0 || end > s.length() || begin > end)
throw new IllegalArgumentException("0<=begin<=end<=length");
return s.substring(begin, end);
}
}
Java典型バグの特定法(重要ポイントの深掘り)
NullPointerException を潰す型
- どの変数が null: メッセージと原因行で特定。使用前に null ガード、短絡評価、Objects.requireNonNull、Optional を活用。
- equals の呼び順:
"OK".equals(s)やObjects.equals(a, b)にすると null セーフ。
Objects.requireNonNull(s, "s must not be null");
if (s != null && !s.isBlank()) { /* ... */ }
JavaIndexOutOfBoundsException の境界
- 原則: 0 ≤ index < size、substring は [begin, end)(end 排他)。
- for の条件:
< lengthを使う。<=は範囲外。
for (int i = 0; i < arr.length; i++) { /* ... */ }
JavaArithmeticException と数値の落とし穴
- 整数の 0 除算: 事前チェックで弾く。
- 浮動小数点: 例外にならず Infinity/NaN。
Double.isInfinite/Double.isNaNを確認。 - オーバーフロー:
Math.addExactで検知。
if (b == 0) throw new IllegalArgumentException("divisor must not be 0");
Javaログ設計と後始末:原因を見失わないために
原因を必ず連鎖させる
- 包み直すとき:
new Xxx("msg", cause)の形で元の例外を保持。スタックトレースが繋がる。
try {
dao.save(order);
} catch (java.sql.SQLException e) {
throw new IllegalStateException("保存失敗: id=" + order.id(), e);
}
Javatry-with-resources で後始末を自動化
- クローズ漏れを防ぐ: 例外時でも自動でリソース解放。サプレスト例外も記録される。
import java.nio.file.*;
import java.io.*;
try (var br = Files.newBufferedReader(Path.of("input.txt"))) {
System.out.println(br.readLine());
}
Java例題で身につける
例 1: スタックトレースから原因行へ直行する
public class Sample {
public static void main(String[] args) {
String s = null;
System.out.println(s.length()); // ← スタックトレースの行番号に一致
}
}
Java実行して例外を出し、行番号の場所を開く。参照が null である事実を確認し、使用前のガードを追加する。
例 2: 条件付きブレークポイントでピンポイント停止
for (String id : ids) {
// id が "X-999" のときだけ止める条件付きブレークポイント
process(id);
}
Java大量データでも、問題のIDにだけ止めて原因を観測できる。
例 3: ログで「何が・どこで」を固定
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(App.class);
void run(String path) {
try {
String line = readFirst(java.nio.file.Path.of(path));
log.info("read_ok path={} len={}", path, line.length());
} catch (Exception e) {
log.error("read_failed path={}", path, e); // 例外連鎖ごと記録
}
}
Java仕上げのアドバイス(重要部分のまとめ)
デバッグの基本は「再現条件の固定」「スタックトレースの先頭行から読む」「IDEで止めて観測する」「最小ケースに切り出す」の4点です。NPE・IOBE・算術の落とし穴は型どおりに潰し、ログは原因連鎖と入力文脈を必ず残す。後始末は try-with-resources で自動化し、例外の包み直しでは cause を渡す。思い込みではなく事実を観測し、仮説→検証→修正→再テストを短いサイクルで回す——この型が身につけば、バグは「すぐ見つけて、すぐ直せる」相手になります。
