catch (Exception e) で握りつぶす
─ 何でもキャッチして catch ブロックが空っぽ ({} だけ) になっているコード。例外が完全に消えてしまい、本番でバグが起きても原因を追えません。最低でも e.printStackTrace(); か業務ログ出力を入れる、というのが鉄則です
レッスンの本文で注意している点を、つまずきの側から33件ぶん並べ直しました。書いた覚えのある形が見つかったら、 そのままレッスンに戻って前後を読めます。
catch (Exception e) で握りつぶす─ 何でもキャッチして catch ブロックが空っぽ ({} だけ) になっているコード。例外が完全に消えてしまい、本番でバグが起きても原因を追えません。最低でも e.printStackTrace(); か業務ログ出力を入れる、というのが鉄則です
stacktrace を捨てる─ catch (Exception e) { throw new RuntimeException("失敗"); } のように、元の e を渡さずに新しい例外を作るパターン。これだと「失敗」とだけ書かれた例外が飛んで、本当の原因 (どのファイルの何行目で何が起きたか) が消えます。必ず throw new RuntimeException("失敗", e); のように原因を引き継ぎましょう
catch の順序を間違える─ 複数の catch を並べるときは、catch (Exception e) のような広いものを後ろに書きます。先頭に書くと、その後ろの catch (NumberFormatException e) は「絶対に到達できないコード」とみなされてコンパイルエラーになります。狭い型から広い型へ、の順番が原則です
Exception を一番上に書いて到達不可コードを作るcatch (Exception e) を最初に置き、その下に catch (NumberFormatException e) を書くと、後者には永遠に到達しません。Java はビルドエラー (exception has already been caught) で止めてくれますが、原因がピンと来ないと数十分溶かしがちです。順序は 具体 → 抽象 が鉄則です
catch でもみ消して原因が分からなくなるcatch (Exception e) { /* 何もしない */ } のようにブロックを空にすると、例外が握り潰されて呼び出し側が異常に気付けません。最低でも System.err.println(e) でログを出すか、別の例外で再 throw するのが鉄則です
finally の中で return してしまうfinally の中で return を書くと、try や catch 内で書いた return が 上書きされて消える という非直感的な挙動になります。finally には後始末の処理だけを書き、return は try/catch 側にまとめておくのが安全です
throws を書き忘れて検査例外を投げるFileReader などを使うコードを書いて throws IOException を忘れると、unreported exception IOException; must be caught or declared to be thrown というエラーが出ます。コンパイラのメッセージのとおり、try-catch か throws のどちらかを必ず書きましょう
catch (Exception e) {} で握り潰す例外を catch しただけで中身を空にすると、エラーが起きてもログにも出ず、原因の追跡が極めて困難になります。最低でも e.printStackTrace() を呼ぶか、throw new RuntimeException(e) のように包み直して再送出するのが鉄則です
throws を書くthrows NullPointerException のように非検査例外を throws 句に書いても、コンパイラは強制してくれませんし、可読性も上がりません。Javadoc の @throws タグでドキュメントに書く方が読みやすいです
Exception を継承して検査例外まみれになる何の気なしに extends Exception と書くと、その例外を投げるメソッドには throws InsufficientFundsException の宣言が必要になります。呼び出し元、その呼び出し元、と上に向かってずっと宣言が伝染し、ラムダや Stream の中で投げられなくなる、という悲劇が起きがちです。意図的に検査例外にしたいのでなければ RuntimeException 継承にしましょう
super(message) を呼び忘れて getMessage() が null になるコンストラクタの中で super(message) を呼ばないと、親クラスにメッセージが伝わりません。後で e.getMessage() を見たら null で原因が全く分からない、というハマり方をします。引数で受け取った message は必ず super(message) (もしくは super(message, cause)) で渡してください
catch (Exception e) {} で握り潰すカスタム例外を作っておきながら、上の層で catch (Exception e) {} のように何もしないと、例外を作った意味が完全に消えます。最低でも e.printStackTrace() か、構造化されたロガー (SLF4J の logger.error(...)) で出力するクセを付けましょう。投げる例外と捕まえる例外は、必ずペアで設計するのが鉄則です
implements AutoCloseable を忘れるtry (Resource r = ...) の Resource クラスが AutoCloseable を実装していないと、required type that implements AutoCloseable というコンパイルエラーになります。class Resource implements AutoCloseable と必ず書きましょう
close() の中で重い処理や例外を派手に投げるclose() は「後始末」のメソッドです。ここでネットワーク通信をしたり、独自の例外を派手に投げたりすると、デバッグが非常に難しくなります。close() は冪等 (idempotent) に、つまり何回呼んでも安全になるよう書くのが鉄則です
try の () の中で複数リソースを書く順番を間違えるclose() は宣言と逆順で呼ばれるので、依存関係 (たとえば Connection → Statement → ResultSet) がある場合は、依存される側を先に書き、依存する側を後に書きます。Connection Statement ResultSet の順に宣言すれば、逆順で ResultSet → Statement → Connection の順に閉じられる、ということです
finally の中で return を書いて例外を握りつぶすtry から飛んできた例外を catch していないのに、finally で return してしまうと、本来呼び出し元に伝わるはずだった例外が消えてしまいます。デバッグ時に「エラーがどこかで握りつぶされている」と気づくのが非常に難しくなるので、finally の中で return するのは原則禁止と覚えておきます
finally で参照する変数を初期化していないtry ブロックの中だけで宣言した変数は、finally のスコープ外です。reader のようなリソース変数は try の 外側 で宣言し、null で初期化しておき、finally で if (reader != null) reader.close(); のようにチェックしてから使うのが定石です
try の最後に書いて、例外時に呼ばれない「reader.close() を try ブロックの最後の行に書けば良い」と思いがちですが、try 中で例外が出るとそこから先は実行されません。close() が呼ばれず、ファイルハンドルやコネクションがリークします。リソース解放は finally、もしくは try-with-resources に必ず移す のが鉄則です
close() を呼び忘れる / 古い書き方をするnew FileWriter(...) 系の古い API を使うと、finally で close() を書かないとリソースが残ります。try-with-resources (try (var w = new FileWriter(...)) { ... }) を必ず使うか、いっそ Files.writeString に置き換えるのが正解です
古い new FileWriter("a.txt") は OS のデフォルトエンコーディングで書き込むので、Windows (Shift_JIS) と Linux (UTF-8) で結果が変わることがあります。Files.writeString は常に UTF-8 なので安全です
IOException を握りつぶすファイル操作は失敗する可能性が常にあります (権限・容量・パスの存在)。catch (IOException e) {} のように空ブロックで握りつぶすと、トラブルが起きても原因が分からなくなるので、必ず e.printStackTrace() か throw new RuntimeException(e) で見える形にしましょう
readAllLines を使うreadAllLines は全行を一気にメモリへロードします。1GB のログを読もうとすると OutOfMemoryError で死にます。サイズ感が分からない場合は最初から Files.lines + Stream で書く癖を付けるのが安全です
Java 8 以降は引数なしで UTF-8 ですが、ファイル自体が Shift_JIS で保存されていれば文字化けします。読み込みが化けたら、まずファイルのエンコーディングを疑い Charset.forName("Shift_JIS") を渡してみましょう
NullPointerException を期待するFiles.readAllLines は空ファイルでも null ではなく 空の List を返します。if (lines == null) のような防衛は不要で、代わりに if (lines.isEmpty()) で判定するのが正解です
String の + でパスを連結するdir + "/" + name のように書くと Windows では / と \ が混ざり、name 側に余計な / が含まれていると // ができます。必ず dir.resolve(name) を使いましょう
"C:\\Users\\taro" のような書き方は Linux で動きません。マルチプラットフォームで動かしたいコードでは Paths.get("C:", "Users", "taro") のように分割して渡すか、設定ファイルから読み込みます
resolve と + を同じ感覚で使うresolve に絶対パスを渡すと「左辺を捨てて絶対パスを返す」という挙動になります。これは仕様であり、+ の単純連結とは違うので注意してください。期待しない上書きを避けたい場合は、事前に child.isAbsolute() を確認します
Stream を閉じ忘れるFiles.lines(p).mapToInt(...).sum() のようにワンライナーで書いてしまうと、Stream が close されずファイルディスクリプタがリークします。少量なら気づきにくいですが、ループの中で何回も呼ぶと「Too many open files」エラーで突然落ちます。必ず try (Stream<String> lines = Files.lines(p)) { ... } の形にしましょう
NumberFormatExceptionInteger.parseInt("") は実行時例外を投げます。改行コードの違いや末尾改行で空文字列が混ざることがあるので、filter(s -> !s.isEmpty()) を挟むか、s -> s.trim().isEmpty() のように空白だけの行も弾く処理を入れると安心です
Files.lines(p) は引数 1 つだと UTF-8 を仮定します。日本語を含む Shift_JIS のファイルなどを読もうとすると MalformedInputException が出ます。明示的に指定したいときは Files.lines(p, StandardCharsets.UTF_8) のように 2 引数版を使い、入力が Shift_JIS なら Charset.forName("MS932") を渡します
exists の戻り値を盲信する (TOCTOU)Files.exists(p) で true を取った直後に別のプロセスがファイルを消すと、続く Files.delete(p) が NoSuchFileException を投げます。これは Time Of Check / Time Of Use の頭文字を取って TOCTOU 問題と呼ばれ、確実に避けたいケースでは Files.deleteIfExists(p) を使うか、例外を try で受け止めるのが定石です
Files.delete と Files.deleteIfExists を混同する前者はファイルが無いと NoSuchFileException、後者は無くても静かに false を返します。「無いなら無いで OK」のときは後者、「絶対あるはず」のときは前者、と用途で使い分けましょう
Files.delete でディレクトリを消そうとして失敗する中身が残っているディレクトリは DirectoryNotEmptyException で消せません。再帰的に消したいときは Files.walk(p) で全ファイルを取り出してから逆順に Files.delete する、もしくは Files.walkFileTree を使うのが定石です