Java中級:例外処理とファイル入出力のよくある間違い

レッスンの本文で注意している点を、つまずきの側から33件ぶん並べ直しました。書いた覚えのある形が見つかったら、 そのままレッスンに戻って前後を読めます。

try-catch で例外をつかむ

レッスンを読む

catch (Exception e) で握りつぶす

─ 何でもキャッチして catch ブロックが空っぽ ({} だけ) になっているコード。例外が完全に消えてしまい、本番でバグが起きても原因を追えません。最低でも e.printStackTrace(); か業務ログ出力を入れる、というのが鉄則です

try-catch で例外をつかむで詳しく読む

stacktrace を捨てる

─ catch (Exception e) { throw new RuntimeException("失敗"); } のように、元の e を渡さずに新しい例外を作るパターン。これだと「失敗」とだけ書かれた例外が飛んで、本当の原因 (どのファイルの何行目で何が起きたか) が消えます。必ず throw new RuntimeException("失敗", e); のように原因を引き継ぎましょう

try-catch で例外をつかむで詳しく読む

catch の順序を間違える

─ 複数の catch を並べるときは、catch (Exception e) のような広いものを後ろに書きます。先頭に書くと、その後ろの catch (NumberFormatException e) は「絶対に到達できないコード」とみなされてコンパイルエラーになります。狭い型から広い型へ、の順番が原則です

try-catch で例外をつかむで詳しく読む

複数の catch ブロック

レッスンを読む

Exception を一番上に書いて到達不可コードを作る

catch (Exception e) を最初に置き、その下に catch (NumberFormatException e) を書くと、後者には永遠に到達しません。Java はビルドエラー (exception has already been caught) で止めてくれますが、原因がピンと来ないと数十分溶かしがちです。順序は 具体 → 抽象 が鉄則です

複数の catch ブロックで詳しく読む

catch でもみ消して原因が分からなくなる

catch (Exception e) { /* 何もしない */ } のようにブロックを空にすると、例外が握り潰されて呼び出し側が異常に気付けません。最低でも System.err.println(e) でログを出すか、別の例外で再 throw するのが鉄則です

複数の catch ブロックで詳しく読む

finally の中で return してしまう

finally の中で return を書くと、try や catch 内で書いた return が 上書きされて消える という非直感的な挙動になります。finally には後始末の処理だけを書き、return は try/catch 側にまとめておくのが安全です

複数の catch ブロックで詳しく読む

throws と例外の伝播

レッスンを読む

throws を書き忘れて検査例外を投げる

FileReader などを使うコードを書いて throws IOException を忘れると、unreported exception IOException; must be caught or declared to be thrown というエラーが出ます。コンパイラのメッセージのとおり、try-catch か throws のどちらかを必ず書きましょう

throws と例外の伝播で詳しく読む

catch (Exception e) {} で握り潰す

例外を catch しただけで中身を空にすると、エラーが起きてもログにも出ず、原因の追跡が極めて困難になります。最低でも e.printStackTrace() を呼ぶか、throw new RuntimeException(e) のように包み直して再送出するのが鉄則です

throws と例外の伝播で詳しく読む

非検査例外に throws を書く

throws NullPointerException のように非検査例外を throws 句に書いても、コンパイラは強制してくれませんし、可読性も上がりません。Javadoc の @throws タグでドキュメントに書く方が読みやすいです

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(...)) で出力するクセを付けましょう。投げる例外と捕まえる例外は、必ずペアで設計するのが鉄則です

独自の例外を作るで詳しく読む

try-with-resources

レッスンを読む

implements AutoCloseable を忘れる

try (Resource r = ...) の Resource クラスが AutoCloseable を実装していないと、required type that implements AutoCloseable というコンパイルエラーになります。class Resource implements AutoCloseable と必ず書きましょう

try-with-resourcesで詳しく読む

close() の中で重い処理や例外を派手に投げる

close() は「後始末」のメソッドです。ここでネットワーク通信をしたり、独自の例外を派手に投げたりすると、デバッグが非常に難しくなります。close() は冪等 (idempotent) に、つまり何回呼んでも安全になるよう書くのが鉄則です

try-with-resourcesで詳しく読む

try の () の中で複数リソースを書く順番を間違える

close() は宣言と逆順で呼ばれるので、依存関係 (たとえば Connection → Statement → ResultSet) がある場合は、依存される側を先に書き、依存する側を後に書きます。Connection Statement ResultSet の順に宣言すれば、逆順で ResultSet → Statement → Connection の順に閉じられる、ということです

try-with-resourcesで詳しく読む

finally ブロック

レッスンを読む

finally の中で return を書いて例外を握りつぶす

try から飛んできた例外を catch していないのに、finally で return してしまうと、本来呼び出し元に伝わるはずだった例外が消えてしまいます。デバッグ時に「エラーがどこかで握りつぶされている」と気づくのが非常に難しくなるので、finally の中で return するのは原則禁止と覚えておきます

finally ブロックで詳しく読む

finally で参照する変数を初期化していない

try ブロックの中だけで宣言した変数は、finally のスコープ外です。reader のようなリソース変数は try の 外側 で宣言し、null で初期化しておき、finally で if (reader != null) reader.close(); のようにチェックしてから使うのが定石です

finally ブロックで詳しく読む

リソース解放を try の最後に書いて、例外時に呼ばれない

「reader.close() を try ブロックの最後の行に書けば良い」と思いがちですが、try 中で例外が出るとそこから先は実行されません。close() が呼ばれず、ファイルハンドルやコネクションがリークします。リソース解放は finally、もしくは try-with-resources に必ず移す のが鉄則です

finally ブロックで詳しく読む

Files でファイルに書き込む

レッスンを読む

close() を呼び忘れる / 古い書き方をする

new FileWriter(...) 系の古い API を使うと、finally で close() を書かないとリソースが残ります。try-with-resources (try (var w = new FileWriter(...)) { ... }) を必ず使うか、いっそ Files.writeString に置き換えるのが正解です

Files でファイルに書き込むで詳しく読む

エンコーディングを指定し忘れる

古い new FileWriter("a.txt") は OS のデフォルトエンコーディングで書き込むので、Windows (Shift_JIS) と Linux (UTF-8) で結果が変わることがあります。Files.writeString は常に UTF-8 なので安全です

Files でファイルに書き込むで詳しく読む

IOException を握りつぶす

ファイル操作は失敗する可能性が常にあります (権限・容量・パスの存在)。catch (IOException e) {} のように空ブロックで握りつぶすと、トラブルが起きても原因が分からなくなるので、必ず e.printStackTrace() か throw new RuntimeException(e) で見える形にしましょう

Files でファイルに書き込むで詳しく読む

ファイルを行単位で読む

レッスンを読む

巨大ファイルに 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()) で判定するのが正解です

ファイルを行単位で読むで詳しく読む

Path でファイルパスを扱う

レッスンを読む

String の + でパスを連結する

dir + "/" + name のように書くと Windows では / と \ が混ざり、name 側に余計な / が含まれていると // ができます。必ず dir.resolve(name) を使いましょう

Path でファイルパスを扱うで詳しく読む

OS 依存の区切り文字を直書きする

"C:\\Users\\taro" のような書き方は Linux で動きません。マルチプラットフォームで動かしたいコードでは Paths.get("C:", "Users", "taro") のように分割して渡すか、設定ファイルから読み込みます

Path でファイルパスを扱うで詳しく読む

resolve と + を同じ感覚で使う

resolve に絶対パスを渡すと「左辺を捨てて絶対パスを返す」という挙動になります。これは仕様であり、+ の単純連結とは違うので注意してください。期待しない上書きを避けたい場合は、事前に child.isAbsolute() を確認します

Path でファイルパスを扱うで詳しく読む

BufferedReader と Stream API

レッスンを読む

Stream を閉じ忘れる

Files.lines(p).mapToInt(...).sum() のようにワンライナーで書いてしまうと、Stream が close されずファイルディスクリプタがリークします。少量なら気づきにくいですが、ループの中で何回も呼ぶと「Too many open files」エラーで突然落ちます。必ず try (Stream<String> lines = Files.lines(p)) { ... } の形にしましょう

BufferedReader と Stream APIで詳しく読む

空行のパースで NumberFormatException

Integer.parseInt("") は実行時例外を投げます。改行コードの違いや末尾改行で空文字列が混ざることがあるので、filter(s -> !s.isEmpty()) を挟むか、s -> s.trim().isEmpty() のように空白だけの行も弾く処理を入れると安心です

BufferedReader と Stream APIで詳しく読む

エンコーディングの指定を忘れる

Files.lines(p) は引数 1 つだと UTF-8 を仮定します。日本語を含む Shift_JIS のファイルなどを読もうとすると MalformedInputException が出ます。明示的に指定したいときは Files.lines(p, StandardCharsets.UTF_8) のように 2 引数版を使い、入力が Shift_JIS なら Charset.forName("MS932") を渡します

BufferedReader と Stream APIで詳しく読む

ファイルの存在確認と削除

レッスンを読む

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 を使うのが定石です

ファイルの存在確認と削除で詳しく読む