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 を書くと、trycatch 内で書いた return が 上書きされて消える という非直感的な挙動になります。finally には後始末の処理だけを書き、returntry/catch 側にまとめておくのが安全です

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

throws と例外の伝播

レッスンを読む

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

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

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() か、構造化されたロガー (SLF4Jlogger.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() は宣言と逆順で呼ばれるので、依存関係 (たとえば ConnectionStatementResultSet) がある場合は、依存される側を先に書き、依存する側を後に書きます。Connection Statement ResultSet の順に宣言すれば、逆順で ResultSetStatementConnection の順に閉じられる、ということです

try-with-resourcesで詳しく読む

finally ブロック

レッスンを読む

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

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

finally ブロックで詳しく読む

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

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

finally ブロックで詳しく読む

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

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

finally ブロックで詳しく読む

Files でファイルに書き込む

レッスンを読む

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

new FileWriter(...) 系の古い API を使うと、finallyclose() を書かないとリソースが残ります。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() のようにワンライナーで書いてしまうと、Streamclose されずファイルディスクリプタがリークします。少量なら気づきにくいですが、ループの中で何回も呼ぶと「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.deleteFiles.deleteIfExists を混同する

前者はファイルが無いと NoSuchFileException、後者は無くても静かに false を返します。「無いなら無いで OK」のときは後者、「絶対あるはず」のときは前者、と用途で使い分けましょう

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

Files.delete でディレクトリを消そうとして失敗する

中身が残っているディレクトリは DirectoryNotEmptyException で消せません。再帰的に消したいときは Files.walk(p) で全ファイルを取り出してから逆順に Files.delete する、もしくは Files.walkFileTree を使うのが定石です

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