1. エラーハンドリング: 例外を無視しない!
ファイル操作は常に外部世界とのやり取りであり、しばしば予測不能です。ディスクは満杯になることがあり、ファイルは消え、権限は変わり、ユーザーは思いもよらないことをします(たとえば、名前にスペースを含むフォルダや、いまにも抜かれそうな USB メモリからあなたのプログラムを起動するなど)。コードがそれに備えていなければ、単に「落ちる」だけでなく、ユーザーの必要なデータを失わせてしまう危険があります。
ベストプラクティス は単なる「流行のアドバイス」ではなく、時間の試練に耐えた手法の集合で、最悪のシナリオを避けるのに役立ちます。たとえばデータ損失、メモリリーク、機密情報の露出、そして後で同僚(とりわけユーザー)の目を見られなくなるような愚かなバグなどです。
なぜ空の catch を書いてはいけないのか?
Java(に限りませんが)では、次のように書きたくなることがあります。
try {
// ファイル操作
} catch (IOException e) {
// まあ、失敗しても気にしない!
}
これは最悪のやり方です。こうしたコードはエラーを「飲み込む」だけでなく、ユーザーにも自分たちにも見えなくしてしまいます。結果として、何がいつ問題になったのか、永遠にわからないままです。
正しいやり方は?
- エラーを記録する: 少なくともコンソールに出力するか、ログファイルに書きましょう.
- ユーザーに知らせる: 重大なエラーであれば、親しみやすいメッセージを表示しましょう.
- 余計な情報を開示しない: ユーザーにシステムの内部詳細を見せないでください(たとえば完全なスタックトレースは開発者向けです)。
例:
try {
List<String> lines = Files.readAllLines(Path.of("data.txt"));
// データ処理
} catch (IOException e) {
System.err.println("ファイルの読み取り中にエラーが発生しました: " + e.getMessage());
// 詳細はログファイルに記録してもよい
e.printStackTrace(System.err);
}
なぜ特定の例外を個別に捕捉することが重要なのか?
エラーによって適切な対応は異なるからです。たとえばファイルが見つからない場合は、別のファイルを選んでもらうよう促せます(NoSuchFileException や FileNotFoundException)。権限がない場合は、必要な権限でプログラムを実行してもらう必要があります(AccessDeniedException)。ディスクが一杯なら、空き領域の確保を促します(書き込み時の IOException など)。
2. アクセス権とセキュリティ
操作の前にアクセス権を確認する
ファイルを読み書きする前に、その権限があるか確認すると良いでしょう。Java には次のメソッドがあります。
- File.canRead()
- File.canWrite()
これらのメソッドが true を返しても成功は保証されません。権限はいつでも変わり得るからです(例えば、別のプロセスが権限を変更した)。したがって常に例外に備えてください。
例:
File file = new File("config.properties");
if (!file.canRead()) {
System.err.println("ファイルを読み取る権限がありません!");
return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
// ファイルの読み取り
} catch (IOException e) {
System.err.println("読み取りエラー: " + e.getMessage());
}
内部情報を開示しない
プログラムが機密ファイル(例えばパスワード)を扱う場合、ユーザーが見られるエラーメッセージにそれらのファイルのパスや内容を表示しないでください。
3. 重要な処理で相対パスを使わない
相対パス(new File("data.txt"))は、現在の作業ディレクトリに対するパスですが、これはプログラムの起動方法(IDE からか、コマンドラインからか)によって異なる場合があります。混乱や誤りの原因になります。
ベストプラクティス: 重要なファイルには絶対パスを使うか、作業ディレクトリを明示的に決めましょう。
例:
String userHome = System.getProperty("user.home");
Path configPath = Path.of(userHome, "myapp", "config.properties");
4. 一時ファイルとディレクトリの扱い
一時ファイルは何のためにあるのか?
一時ファイルはさまざまな用途に使われます。中間処理のために使うことがあります。例えば、まず一時ファイルにデータを書き込み、その後でそのファイルを本番ファイルに置き換えるといった使い方です。別の用途としては、プログラム終了後に不要となり、安全に削除できる情報の一時的な保管です。
安全に一時ファイルを作成するには?
java.nio.file.Files のメソッドを使いましょう:
Path tempFile = Files.createTempFile("myapp_", ".tmp");
// ... ファイル操作
Files.deleteIfExists(tempFile);
一時ディレクトリ
Path tempDir = Files.createTempDirectory("myapp_");
5. 信頼性: バックアップと整合性チェック
重要なファイルを変更する際はバックアップを使う
重要なファイル(たとえば設定)を上書きする前に、まずコピーを作成しましょう:
Path config = Path.of("config.properties");
Path backup = Path.of("config.properties.bak");
if (Files.exists(config)) {
Files.copy(config, backup, StandardCopyOption.REPLACE_EXISTING);
}
書き込み時に問題が起きても、常にバックアップから復元できます。
データの整合性を確認する
特に重要なデータにはチェックサム(たとえば MD5 や SHA-256)を使えます。ファイルを書き込んだ後にチェックサムを計算して横に保存し、読み込み時にファイルが変わっていないか検証します。
SHA-256 の計算例(暗号が好きな人向け):
import java.security.MessageDigest;
import java.nio.file.Files;
import java.nio.file.Path;
byte[] data = Files.readAllBytes(Path.of("important.dat"));
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data);
// hash を別ファイルに保存するか、読み込み時に比較する
6. チェックと使用の間の隙間を最小化する
これは古典的な TOCTOU(Time Of Check To Time Of Use)の問題です。ファイルの存在を確認した直後から実際に読み始めるまでの間に、ファイルが消えたり変更されたりする可能性があります。
可能な限り、チェックと使用を同じ try ブロック内で行いましょう。そして、直前に確認したとしても必ず例外を処理してください。
例:
Path filePath = Path.of("data.txt");
if (Files.exists(filePath)) {
try (BufferedReader reader = Files.newBufferedReader(filePath)) {
// ファイルの読み取り
} catch (IOException e) {
System.err.println("ファイルの読み取りエラー(ファイルが消えた可能性): " + e.getMessage());
}
}
7. さらにいくつかの有用なヒント
すべてのリソースに try-with-resources を使う
AutoCloseable を実装しているクラス(Java IO/NIO のストリームのほとんど)は、try-with-resources で使えます。これによりリソースリークを防げます。
try (BufferedReader reader = Files.newBufferedReader(Path.of("data.txt"))) {
// 読み取り
}
一時ファイルの削除を忘れない
Files.deleteIfExists(tempFile);
リソースを二重にクローズしない
try-with-resources を使っている場合は、手動で close() を呼ばないでください。エラーや重複クローズの原因になります。
8. ファイル操作での典型的なミス
誤り №1: 例外の無視。
空の catch は、ハエを素手で捕まえてまた放すようなものです。常にログを取るか、少なくともユーザーに何が起きたかを知らせましょう。
誤り №2: ストリームを閉じない。
ストリームを閉じ忘れると、ファイルがロックされたままになったり、システムのファイルディスクリプタが枯渇することがあります。try-with-resources を使いましょう。
誤り №3: 重要なファイルに相対パスを使う。
作業ディレクトリが常に期待通りだと当てにしてはいけません。パスを明示するか、専用ディレクトリ(user.home、java.io.tmpdir)を使う方が安全です。
誤り №4: バックアップなしに重要なファイルを上書きする。
重要なものを消してしまう前にバックアップを作りましょう。あなたの神経とユーザーデータを守ります。
誤り №5: アクセス権を確認しない。
必要なファイルやディレクトリに対してユーザーが読み書き権限を持つか確認しましょう。さもないと、思わぬ AccessDeniedException に遭遇します。
誤り №6: TOCTOU の隙間。
チェックと使用の間に、他の誰かがファイルを変更・削除するかもしれません。確認後であっても必ず例外処理を行いましょう。
誤り №7: 一時ファイルやゴミを残す。
プログラムの異常終了やエラー後に一時ファイルが残ることがあります。特に機微なデータであれば、削除を忘れないでください。
GO TO FULL VERSION