Javaプログラムを開発する際、ファイルやディレクトリの存在を確認する処理は、ファイル操作の基本でありながら非常に重要なステップです。

設定ファイルの読み込み、ログ出力先の確認、ユーザーがアップロードしたデータの処理など、不適切なチェックはプログラムの異常終了や予期せぬ動作を引き起こす原因となります。

Javaには古くからある java.io.File クラスと、Java 7で導入された新APIである java.nio.file.Files クラスの2通りの方法が存在します。

本記事では、これら2つの手法の具体的な使い方から、挙動の違い、さらにはパフォーマンスやセキュリティ面での注意点までを詳しく解説します。

Javaでファイル存在チェックが必要な理由

Javaにおけるファイル操作において、なぜ事前に存在チェックを行う必要があるのでしょうか。

それは、ファイルが存在しない状態で読み込みや書き込みを試みると、FileNotFoundException などの例外が発生し、アプリケーションのフローが中断されてしまうためです。

また、単に「あるか・ないか」だけでなく、それが「ファイルなのか・ディレクトリなのか」、あるいは「読み取り権限があるのか」といった属性情報の確認もセットで行われることが一般的です。

現代的なJava開発では、より詳細な情報を取得でき、柔軟なオプション指定が可能な NIO.2 (New I/O) APIの使用が推奨されていますが、レガシーなシステムでは依然として旧来のメソッドも使われています。

それぞれの特性を理解し、プロジェクトの要件に合わせて最適な手法を選択することが、堅牢なコードを書くための第一歩となります。

従来の File.exists メソッドによる存在チェック

Javaの初期から存在する java.io.File クラスを使用した方法です。

非常にシンプルで直感的に記述できるため、多くのサンプルコードで見かける手法です。

File.exists の基本的な使い方

File クラスのインスタンスを作成し、exists() メソッドを呼び出すことで、ファイルまたはディレクトリが存在するかどうかを boolean 型で取得できます。

Java
import java.io.File;

public class FileExistsExample {
    public static void main(String[] args) {
        // チェック対象のパスを指定
        File file = new File("example.txt");

        // 存在チェックの実行
        if (file.exists()) {
            System.out.println("ファイルまたはディレクトリが存在します。");
            
            // ファイルかディレクトリかの判定
            if (file.isFile()) {
                System.out.println("これはファイルです。");
            } else if (file.isDirectory()) {
                System.out.println("これはディレクトリです。");
            }
        } else {
            System.out.println("指定されたパスは見つかりませんでした。");
        }
    }
}
実行結果
ファイルまたはディレクトリが存在します。
これはファイルです。

File.exists のメリットとデメリット

メリットとしては、コードが簡潔であり、Javaの古いバージョンから最新バージョンまで幅広く動作する互換性の高さが挙げられます。

一方で、デメリットも存在します。

エラー詳細の欠如

何らかの理由(アクセス権限不足など)でファイルの有無が確認できなかった場合、例外を投げずに単に false を返します。

シンボリックリンクの扱い

リンク先が存在しない場合の挙動を細かく制御することが困難です。

スケーラビリティ

大量のファイルを高速に処理する必要がある場合、後述する NIO.2 に比べてパフォーマンスが劣る場合があります。

現代的な手法 Files.exists (NIO.2) による存在チェック

Java 7以降、標準的に推奨されているのが java.nio.file.Files クラスを使用した存在チェックです。

このクラスは Path インタフェースと組み合わせて使用します。

Files.exists の基本的な使い方

Files.exists(Path path, LinkOption... options) は、第2引数にオプションを指定することで、シンボリックリンクの扱いを変更できるなど、より高度な制御が可能です。

Java
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.LinkOption;

public class FilesExistsExample {
    public static void main(String[] args) {
        // Pathオブジェクトの生成
        Path path = Paths.get("data/config.properties");

        // 基本的な存在チェック
        if (Files.exists(path)) {
            System.out.println("指定されたパスが存在します。");
        } else {
            System.out.println("指定されたパスは存在しません。");
        }

        // シンボリックリンクを辿らずに存在確認する場合
        boolean existsNoFollow = Files.exists(path, LinkOption.NOFOLLOW_LINKS);
        System.out.println("リンクを辿らないチェック結果: " + existsNoFollow);
    }
}
実行結果
指定されたパスが存在します。
リンクを辿らないチェック結果: true

Files.notExists メソッドの役割

Files クラスには、存在しないことを確認するための notExists メソッドも用意されています。

!Files.exists(path) と何が違うのか?」という疑問を持つ方も多いでしょう。

実は、ファイルの状態には「存在する」「存在しない」の他に、「状態が不明(アクセス権限などで判定不能)」という第3の状態があります。

メソッド存在する場合存在しない場合状態不明の場合
Files.exists()truefalsefalse
Files.notExists()falsetruefalse

このように、Files.existsfalse だからといって、必ずしも「ファイルが存在しない」とは言い切れないケースがあるため、厳密な判定が必要な場合は両方のメソッドの結果を考慮する必要があります。

どちらを使うべきか? File vs Files の比較

既存の古いコードを保守している場合を除き、基本的には Files.exists を使用することが推奨されます

その理由を比較表でまとめました。

特徴File.exists (java.io)Files.exists (java.nio.file)
導入時期Java 1.0 (レガシー)Java 7 (NIO.2)
例外処理例外を投げず単に false を返す詳細な設定やオプションが可能
シンボリックリンク常にリンク先を追跡する追跡するかどうかを選択可能
メタデータ取得限定的。別途複数の呼び出しが必要効率的に一括取得が可能
エラーの明確さ不透明(権限エラーも存在なしと同じ)権限不足と存在なしを区別できる

パフォーマンスに関する注意点

小規模なアプリケーションでは気にする必要はありませんが、数万件以上のファイルをループ内でチェックする場合、Files.exists は内部的に詳細なファイル属性を取得しようとするため、File.exists よりもわずかにオーバーヘッドが大きくなるケースがあります。

しかし、現代のストレージ環境では無視できる差であることが多く、コードの安全性と保守性の観点から Files クラスを使うのがベストプラクティスです。

実践的なファイル存在チェックのシナリオ

単に存在を確認するだけでなく、実際の開発でよく遭遇するパターン別の実装方法を紹介します。

ディレクトリであるかを確認する

ファイルが存在していても、それがディレクトリ(フォルダ)であった場合にエラーにしたい、あるいはその逆のケースは多々あります。

Java
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class DirectoryCheck {
    public static void main(String[] args) {
        Path path = Paths.get("logs");

        if (Files.isDirectory(path)) {
            System.out.println("ログ出力先ディレクトリを確認しました。");
        } else if (Files.isRegularFile(path)) {
            System.out.println("警告: logs はディレクトリではなくファイルとして存在します。");
        } else {
            System.out.println("ディレクトリが存在しません。作成を開始します。");
        }
    }
}

読み取り・書き込み権限を確認する

存在していても、プログラムからアクセスできなければ意味がありません。

Java
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class PermissionCheck {
    public static void main(String[] args) {
        Path configPath = Paths.get("config/app.conf");

        if (Files.exists(configPath)) {
            if (Files.isReadable(configPath)) {
                System.out.println("設定ファイルを読み込みます。");
            } else {
                System.err.println("エラー: 設定ファイルに読み取り権限がありません。");
            }
        }
    }
}

注意点:チェックと実行の間の競合(TOCTOU)

ファイル存在チェックにおける最も高度で重要な注意点が、TOCTOU (Time-of-Check to Time-of-Use) と呼ばれる競合状態の問題です。

これは、「ファイルが存在することを確認した瞬間」と「実際にファイルを開く瞬間」の間に、別のプロセスによってファイルが削除されたり、名前が変更されたりする可能性があることを指します。

悪い例

Java
if (Files.exists(path)) {
    // ここで他のプロセスがファイルを削除すると、次の行でエラーになる
    List<String> lines = Files.readAllLines(path); 
}

推奨されるアプローチ

存在チェックを事前に行うのは「親切なエラーメッセージを表示するため」と割り切り、本質的な処理は 「例外処理(try-catch)」で囲む ことです。

Java
try {
    // 存在チェックを飛ばして、いきなり操作を試みる
    List<String> lines = Files.readAllLines(path);
} catch (NoSuchFileException e) {
    System.err.println("ファイルが見つかりません: " + e.getFile());
} catch (IOException e) {
    System.err.println("ファイルの読み込み中にエラーが発生しました: " + e.getMessage());
}

このように、Files.readAllLinesFiles.newInputStream などは、ファイルが存在しない場合に適切な例外を投げてくれるため、存在チェックの有無にかかわらず、必ず例外ハンドリングを行うようにしましょう。

Java 8以降の便利なテクニック

Stream APIと組み合わせることで、特定のディレクトリ内に存在する特定のファイルのみを抽出して処理するといった操作もスマートに記述できます。

Java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;

public class StreamFileCheck {
    public static void main(String[] args) {
        Path dir = Paths.get("data");

        try (Stream<Path> stream = Files.list(dir)) {
            stream.filter(Files::isRegularFile) // ファイルのみ
                  .filter(p -> p.toString().endsWith(".csv")) // CSVのみ
                  .forEach(p -> System.out.println("処理対象: " + p.getFileName()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

このコードでは、Files.list を使用してディレクトリ内を走査し、各要素に対して Files::isRegularFile(メソッド参照)を適用して存在と種類の確認を同時に行っています。

よくあるトラブルと解決策

ファイル存在チェックでハマりやすいポイントをまとめました。

相対パスの基準位置

Paths.get("test.txt") と書いた場合、そのファイルは「カレントディレクトリ(実行時のディレクトリ)」から探されます。

IDE(EclipseやIntelliJ)から実行する場合と、jarファイルとしてコマンドラインから実行する場合では基準位置が異なることがあるため、path.toAbsolutePath() を出力して、プログラムがどこを探しているのかを確認するのが有効です。

大文字・小文字の区別

Windows環境ではファイル名の大文字・小文字は区別されませんが、Linux環境(サーバー環境など)では厳密に区別されます。

開発環境(Windows)で動いても本番環境(Linux)でexists()false を返す場合、この差異が原因であることが多いです。

ネットワークドライブ上のファイル

ネットワーク上の共有フォルダにあるファイルをチェックする場合、OSレベルでのマウント状態やタイムアウトにより、ファイルが存在するにもかかわらずfalse が返ったり、メソッドの返却まで数秒待たされたりすることがあります。

まとめ

Javaでファイル存在チェックを行う際は、以下のポイントを意識しましょう。

  • 基本的には Files.exists (NIO.2) を使用するのが現代的な標準。
  • 存在しないことを確認したい場合は、単純な否定(!)ではなく、Files.notExists の使用も検討する。
  • シンボリックリンクをどう扱うかに応じて LinkOption を適切に指定する。
  • 存在チェック後の競合状態(TOCTOU)を防ぐため、実際の操作は必ず try-catch ブロック で実装する。
  • ディレクトリ判定(isDirectory)や権限確認(isReadable)を組み合わせ、より詳細なバリデーションを行う。

ファイル操作はアプリケーションの安定性に直結する部分です。

今回紹介した Files クラスのメソッドを使いこなし、エラーに強く、メンテナンス性の高いJavaプログラムの実装を目指してください。