効率的なデバッグ例外
他の言語: English Español 한국어 Português 中文
Javaとの調教で、最初に学ぶべき概念の1つは exceptions(例外)です。この用語は、プログラムの実行中に予期せぬシナリオを定義します。 たとえば、ネットワークの失敗や予期せぬファイルの終わりなどです。 また、Oracleのドキュメンテーションによると:
Exception クラスとその下位クラスは、適切なアプリケーションが捕捉したいと思う状態を示す形式の Throwable です。
プログラムがこれらの状況を効果的に処理する「プランb」を装備している場合、 それらのいずれかが発生してもスムーズに動作を続けます。 そうでなければ、プログラムは予期せぬクラッシュを起こすか、不正な状態になる可能性があります。
プログラムが例外のために失敗した場合、デバッグする必要があります。 プログラミング言語は、例外関連のエラーのデバッグを容易にするために、 スタックトレースという特殊なメッセージを提供しています。これは、失敗につながったコードパスを指しています。 この情報は非常に有用であり、時には十分であることがあります。 しかし、追加の詳細や技術が役立つ場合もあります。

この記事では、JSONパース中に発生する例外をデバッグする ケーススタディを通じて、スタックトレースの見方だけでなく、 デバッガーの使用の利点を発見することに焦点をあてていきます。
サンプルアプリケーション
このユースケースのサンプルアプリケーションは、 世界中の空港に関するデータが含まれた一連のJSONファイルを解析する小さなJavaプログラムです。 ファイルには、空港のIATAコード、国、緯度、経度などの詳細が含まれています。以下は、エントリの例です:
{
"iso_country": "AR",
"iata_code": "MDQ",
"longitude_deg": "-57.5733",
"latitude_deg": "-37.9342",
"elevation_ft": "72",
"name": "Ástor Piazzola International Airport",
"municipality": "Mar del Plata",
"iso_region": "AR-B"
}
プログラムは非常にシンプルです。一連のファイルを走査し、
それぞれを読み取ってパースし、空港オブジェクトを
入力制限に対してフィルタリングします。例えば、"country=AR"
といった制限です。その後、一致する空港のリストを出力します:
public class Airports {
static Path input = Paths.get("./data");
static Gson gson = new Gson();
static {
System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8));
}
public static void main(String[] args) throws IOException {
List<Predicate<Airport>> filters = new ArrayList<>();
for (String arg : args) {
if (arg.startsWith("country=")) {
filters.add(airport -> airport.isoCountry.equals(arg.substring("country=".length()))) ;
} else if (arg.startsWith("region=")) {
filters.add(airport -> airport.isoRegion.equals(arg.substring("region=".length()))) ;
} else if (arg.startsWith("municipality=")) {
filters.add(airport -> airport.municipality.equals(arg.substring("municipality=".length()))) ;
}
}
try (Stream<Path> files = Files.list(input)) {
Stream<Airport> airports = files.map(Airports::parse);
for (Predicate<Airport> f : filters) {
airports = airports.filter(f);
}
airports.forEach(System.out::println);
}
}
static Airport parse(Path path) {
try {
JsonObject root = gson.fromJson(Files.readString(path), JsonObject.class);
String name = root.get("name").getAsString();
String isoCountry = root.get("isoCountry").getAsString();
String iataCode = root.get("iataCode").getAsString();
String longitudeDeg = root.get("longitudeDeg").getAsString();
String latitudeDeg = root.get("latitudeDeg").getAsString();
String municipality = root.get("municipality").getAsString();
String isoRegion = root.get("isoRegion").getAsString();
Integer elevationFt = Integer.parseInt(root.get("elevationFt").getAsString());
return new Airport(name, isoCountry, iataCode, longitudeDeg, latitudeDeg, elevationFt, municipality, isoRegion);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
record Airport(String name, String isoCountry, String iataCode, String longitudeDeg, String latitudeDeg, Integer elevationFt, String municipality, String isoRegion) { }
}
問題
プログラムを実行すると、 NumberFormatException
によってエラーが発生します。
以下が得られるスタックトレースです:
Exception in thread "main" java.lang.NumberFormatException: For input string: ""
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:672)
at java.base/java.lang.Integer.parseInt(Integer.java:778)
at dev.flounder.Airports.parse(Airports.java:53)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
at dev.flounder.Airports.main(Airports.java:39)
これは、 Airports.java
ファイルの53行目を指しています。
この行を見ると、いずれかの空港のelevationFt
プロパティを数値に変換する際に問題があることがわかります。
例外のメッセージから、このプロパティが対応するファイルでは空白になっているため、ということがわかります。
スタックトレースだけでは十分ではない?
上記のスタックトレースはすでに多くの情報を提供していますが、それにはいくつかの限界があります。 それは失敗するコードの行を指し示しますが、それが原因となるデータファイルは示していますか? ファイル名は、それをより詳しく調査したり、データを修正したりする場合に便利です。 残念ながら、これらのランタイム詳細はアプリケーションがクラッシュすると失われます。
だからと言って、アプリケーションが終了する前にアプリケーションを一時停止する方法はあります。 これによって、スタックトレースやログに記録されていないコンテキストにアクセスすることができます。 さらに、アプリケーションが停止した状態では、プロトタイピング、テスト、 そして修正をロードすることができます。アプリケーションがまだ動いている間にです。
例外での一時停止
まず、例外ブレークポイントの設定を開始しましょう。 これは、ブレークポイントダイアログや、 スタックトレースでCreate breakpointをクリックすることで行うことができます:


このタイプのブレークポイントは、特定の行をターゲットにするのではなく、
例外がスローされる直前に、アプリケーションを一時停止します。
現在、私たちが関心を持っているのは NumberFormatException
です。


したがって、例外がスローされる予定だったところでアプリケーションを一時停止しました。 エラーはすでに発生していますが、アプリケーションはまだクラッシュしていません。 我々はちょうど間に合っています、だからこれが何をもたらしてくれるか見てみましょう。
スレッド (Threads)タブで、 parse()
フレームに移動します。


これで、私たちが持っている情報がどれだけ多いかを見てみましょう:
エラーが発生しているファイル名、
ファイルの完全な内容、すべての近接する変数、
そしてもちろん、エラーを引き起こしているプロパティが見えます。
問題は816.json
というファイルにあることが明らかで、それはelevationFt
プロパティが欠けているからです。
これで、問題を修正することができます。使用ケースによっては、データを修正するだけでいいかもしれないし、 プログラムがエラーを処理する方法を修正するかもしれません。 データの修正は直感的なので、デバッガーがエラーハンドリングにどのように役立つかを見てみましょう。
修正をプロトタイプ化する
expression の評価は、
既存のコードに対する修正など、変更をプロトタイピングするための素晴らしいツールです。
まさに我々が探していたものです。
parse()
メソッド本体全体を選択し、
実行 (Run) | デバッグアクション (Debugging Actions) | 式の評価 (Evaluate Expression)に移動します。
体験の開始 (Evaluate)ダイアログを開く前にコードを選択しておくと、 選択したスニペットがコピーされるので、手動で入力する必要がありません。
私たちがメソッドのコードを評価すると、例外がスローされます。 正確には、プログラムのコードでは例外がスローされます:


コードを少し変更して、データが欠けているときに null
を保存するようにしましょう。
また、エラーを標準エラー出力に出力するための print 文を追加します:
try {
JsonObject root = gson.fromJson(Files.readString(path), JsonObject.class);
String name = root.get("name").getAsString();
String isoCountry = root.get("isoCountry").getAsString();
String iataCode = root.get("iataCode").getAsString();
String longitudeDeg = root.get("longitudeDeg").getAsString();
String latitudeDeg = root.get("latitudeDeg").getAsString();
String municipality = root.get("municipality").getAsString();
String isoRegion = root.get("isoRegion").getAsString();
Integer elevationFt;
try {
elevationFt = Integer.parseInt(root.get("elevationFt").getAsString());
} catch (NumberFormatException e) {
elevationFt = null;
System.err.println("Failed to parse elevation for file: " + path);
}
return new Airport(name, isoCountry, iataCode, longitudeDeg, latitudeDeg, elevationFt, municipality, isoRegion);
} catch (IOException e) {
throw new RuntimeException(e);
}
体験の開始 (Evaluate)ダイアログで調整したコードを実行することで、
このコードが想定通りにエラーを処理することを確認することができます。
今度はアプリケーションをクラッシュさせるのではなく、対応するフィールドをnull
に設定します。


メソッドが例外をスローせずに返ることをテストできるだけでなく、 エラーテキストがコンソールにどのように表示されるかも確認できます。


フレームのリセット
さて、私たちはエラーを修正して、これからはエラーが発生しないようにしましたが、 エラー自体を元に戻すことができるでしょうか?実際には可能です! IntelliJ IDEAのデバッガーは、エラーの発生したフレームをスタックからポップオフして、メソッドを最初から実行することができます。
スレッド (Threads)タブでフレームのリセット (Reset Frame)をクリックします。


フレームのリセット (Reset Frame)はフレームの内部状態だけを巻き戻します。 今回のケースでは、メソッドは純粋であり、ローカルのスコープの外側で何も変更していないため、これは問題ではありません。 しかし、グローバルなアプリケーションの状態に変更が加えられた場合、 これらは元に戻されません。 体験の開始 (Evaluate)やフレームのリセット (Reset Frame)のような機能を使用するときは、そのような効果を考慮に入れてください。
修正とホットリロード
エラーを捨てて、まるでそれが存在しなかったかのように pretending した後、ランタイムに修正を配送することもできます。現在の実行ポイントが変更したいメソッドの外側にあり、メソッドのシグネチャを変更していないため、変更したクラスの再ロード (Reload Changed Classes)オプションを使用することができます。この機能は以前の投稿ですでに取り上げています。
まず、修正したコードを体験の開始 (Evaluate)ダイアログからエディターにコピーします。
以前に評価したコードは、履歴(⌥↓ / Alt↓)を参照することで見つけることができます。
Airports
クラスのコードを入れ替えた後、それを実行中のJVMにロードすることができます。
これには、実行 (Run) | デバッグアクション (Debugging Actions) | 変更したクラスの再ロード (Reload Changed Classes)を選択します。


それがランニング中のアプリケーションに修正が適用されたことを確認するバルーンが表示されます。
未処理の例外フィルタ
現在、アプリケーションを再開すると、同じ例外で再び中断されます。どうしてこんなことが起こるのでしょうか?
我々はコードを修正して NumberFormatException
を捕捉するようにしました。
この修正はアプリケーションのクラッシュを防ぎますが、例外のスローを防ぐわけではありません。
したがって、ブレークポイントは例外が発生するたびに発火します、たとえそれが最終的に捕捉されるとしても。
未処理の例外が発生した場合にのみアプリケーションを一時停止するようにIntelliJ IDEAに指示しましょう。 これには、ガターのブレークポイントを右クリックし、捕捉した例外 (Caught exception)のチェックボックスをクリアします:


例外ブレークポイントのアイコンは、アプリケーションが例外で中断されているときにのみガターに表示されます。 また、ブレークポイントは実行 (Run) | ブレークポイントの表示 (View Breakpoints)を通じて設定することもできます。
この設定では、未処理の NumberFormatException
が発生した場合にのみ、アプリケーションが中断されます。
再開して楽しむ
これで、アプリケーションの再開が可能になります。


それはただの罰金で、コンソールで欠けているデータについて確認できます。
エラーリストに816.json
があることに注意してください。これは、私たちは実際にこのファイルを処理したことを確認するためです。


実際には、816.json
には2つのエントリがあります。1つはexpression evaluationの実験から、もう1つはプログラム自体が parse()
メソッドを修正して再実行した結果です。
まとめ
この投稿では、Javaの例外を効率的にデバッグする方法について学び、その方法は、 失敗の原因を指し示す追加のコンテキストを収集します。その後、デバッガーの機能を利用して、 エラーを元に戻し、デバッグセッションを修正のテスト用のサンドボックスに変え、 クラッシュの最中にランタイムにその修正を提供することを可能にします、 すべてアプリケーションを再起動せずにです。
これらのシンプルな技術は、特定の状況で非常に強力で、時間を大幅に節約することができます。 これからの投稿では、さらに多くのデバッグのヒントやコツを紹介していきますので、お楽しみに!