CodeGym /コース /JAVA 25 SELF /FileVisitor — ファイルシステムの走査と再帰的な操作

FileVisitor — ファイルシステムの走査と再帰的な操作

JAVA 25 SELF
レベル 39 , レッスン 3
使用可能

1. はじめに

まず Files.walk() を使った走査の例を思い出しましょう:

Path start = Paths.get("my-folder");
try (Stream<Path> stream = Files.walk(start)) {
    stream.forEach(System.out::println);
}

ファイルやフォルダを単にたどって何か処理するだけなら、これはとても有効です。しかし、もっと複雑な課題ではどうでしょうか。

  • 空でなくても、フォルダを中身ごと再帰的に削除したい。
  • ディレクトリを再帰的にコピーまたは移動したい。
  • 統計を集めたい(例: すべてのファイルの合計サイズを計算する、拡張子ごとにグループ化する など)。
  • エラーを適切に処理したい(例: あるフォルダにアクセス権がなくても、全体が落ちないようにしたい)。

このような場合は Stream API はあまり便利ではありません。ラムダの中で try-catch を多用したり、走査順序(例: 先にファイルを消してからフォルダを消す)に気を配る必要があり、コードが読みにくくなります。

そこで、これらの用途のために FileVisitor によるファイルシステムツリー走査の仕組みが用意されています。

2. FileVisitor インターフェースの仕組み

インターフェース FileVisitor<T> は、ファイルシステムを走査する際に訪問した各ファイルやフォルダについて通知を受け取る「イベントハンドラ」のようなものです。

Files.walkFileTree(start, visitor) を呼び出すと、Java は指定したパスからツリー走査を開始し、各段階であなたの FileVisitor の該当メソッドを呼び出します。

FileVisitor の主なメソッド

インターフェースの定義は次のとおりです:

public interface FileVisitor<T> {
    FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException;
    FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException;
    FileVisitResult visitFileFailed(T file, IOException exc) throws IOException;
    FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException;
}
  • preVisitDirectory — ディレクトリに入る前に呼ばれます。
  • visitFile — 各ファイルに対して呼ばれます。
  • visitFileFailed — ファイルにアクセスできなかった場合に呼ばれます。
  • postVisitDirectory — ディレクトリを出た後(その配下のファイルやサブディレクトリを処理した後)に呼ばれます。

これらの各メソッドは FileVisitResult を返し、走査の継続方法を指示します:

  • FileVisitResult.CONTINUE — 走査を続行する。
  • FileVisitResult.SKIP_SUBTREE — 現在のディレクトリとその配下をスキップする。
  • FileVisitResult.SKIP_SIBLINGS — 同じ階層にある残りの兄弟ノード(ファイル/フォルダ)をスキップする。
  • FileVisitResult.TERMINATE — 走査を完全に終了する。

SimpleFileVisitor クラス

毎回 4 つのメソッドすべてを実装するのは面倒です。1~2 個だけ必要なことも多いでしょう。そのため Java には便利なアダプタクラス SimpleFileVisitor があります。既定の実装(単に CONTINUE)を提供しており、必要なメソッドだけをオーバーライドできます。

例:

import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class MyVisitor extends SimpleFileVisitor<Path> {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        System.out.println("ファイル: " + file);
        return FileVisitResult.CONTINUE;
    }
}

3. Files.walkFileTree の使い方: 基本例

例 1: すべてのファイルとフォルダを表示する

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class TreePrinter {
    public static void main(String[] args) throws IOException {
        Path start = Paths.get("my-folder");

        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
                System.out.println("フォルダ: " + dir);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                System.out.println("  ファイル: " + file);
                return FileVisitResult.CONTINUE;
            }
        });
    }
}

出力:

フォルダ: my-folder
  ファイル: my-folder/file1.txt
  ファイル: my-folder/file2.txt
フォルダ: my-folder/subdir
  ファイル: my-folder/subdir/nested.txt

ご覧のとおり、走査は深さ優先で進みます。まずフォルダに入ってそのファイルを処理し、その後にサブフォルダへ進みます。

4. 例: ディレクトリの再帰的削除

よくあるタスクの 1 つは、フォルダを中身ごと削除することです。Files.delete(path) でフォルダを削除しようとしても、中が空でなければ例外になります。先にすべてのファイルとサブフォルダを削除し、その後でフォルダ本体を削除する必要があります。

FileVisitor を使うと次のように書けます:

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class RecursiveDelete {
    public static void main(String[] args) throws IOException {
        Path dirToDelete = Paths.get("test-folder");

        Files.walkFileTree(dirToDelete, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Files.delete(file); // ファイルを削除
                System.out.println("削除したファイル: " + file);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                Files.delete(dir); // 中身をすべて削除した後でフォルダを削除
                System.out.println("削除したフォルダ: " + dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }
}

重要なポイント:
フォルダの削除は postVisitDirectory で行います。つまり、中身をすべて削除した後です。もし中のファイルを削除する前にフォルダを削除しようとすると、エラーになります。

5. 例: ディレクトリ配下の全ファイルの合計サイズを計算する

次に、フォルダとそのサブフォルダ内の全ファイルの合計サイズを数える FileVisitor を書いてみましょう。

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class DirectorySizeCalculator {
    private static long totalSize = 0;

    public static void main(String[] args) throws IOException {
        Path start = Paths.get("my-folder");

        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                totalSize += attrs.size();
                return FileVisitResult.CONTINUE;
            }
        });

        System.out.println("合計サイズ: " + totalSize + " バイト");
    }
}

注意:
サイズの蓄積にはフィールド totalSize を使っています。実際のアプリケーションでは static フィールドの使用は避け、オブジェクトを介して値を渡すほうが望ましいですが、例を簡単にするためにこの形にしています。

6. 例: FileVisitor で拡張子(マスク)によるファイル検索

例えば、.txt ファイルをディレクトリとサブディレクトリ内で全て見つけて一覧表示したいとします。

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class TxtFileFinder {
    public static void main(String[] args) throws IOException {
        Path start = Paths.get("my-folder");

        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                if (file.getFileName().toString().endsWith(".txt")) {
                    System.out.println("見つかった .txt ファイル: " + file);
                }
                return FileVisitResult.CONTINUE;
            }
        });
    }
}

見つかったファイルを収集したい場合は、リストを用意します:

import java.util.ArrayList;
import java.util.List;

// main の中:
List<Path> txtFiles = new ArrayList<>();
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        if (file.getFileName().toString().endsWith(".txt")) {
            txtFiles.add(file);
        }
        return FileVisitResult.CONTINUE;
    }
});
System.out.println("見つかった合計: " + txtFiles.size());

7. エラー処理と走査の注意点

ファイルやフォルダにアクセス権がない場合は?

走査中に、権限がない(あるいは他のプロセスがロックしている)ためにアクセスできないファイルやフォルダに遭遇することがあります。この場合は visitFileFailed が呼ばれます。

例:

@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
    System.err.println("ファイルへのアクセスエラー: " + file + " (" + exc + ")");
    return FileVisitResult.CONTINUE; // エラーがあっても走査を続行する
}

エラー時に走査を中断したい場合は、TERMINATE を返します.

フォルダを丸ごとスキップするには?

特定のフォルダ(例えば .gitnode_modules)に入らないようにしたい場合は、preVisitDirectorySKIP_SUBTREE を返します:

@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
    if (dir.getFileName().toString().equals("node_modules")) {
        return FileVisitResult.SKIP_SUBTREE; // このフォルダとそのサブディレクトリに入らない
    }
    return FileVisitResult.CONTINUE;
}

8. 実践: 独自の FileVisitor を実装する

FileVisitor を実装して、次を行うものを作りましょう:

  • ディレクトリとサブディレクトリで拡張子 .java のファイルをすべて見つけ、
  • その件数を数え、
  • それらの合計サイズを計算する。
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class JavaFilesStats {
    public static void main(String[] args) throws IOException {
        Path start = Paths.get("src"); // 例: プロジェクトのソースコード

        class JavaFileVisitor extends SimpleFileVisitor<Path> {
            int count = 0;
            long totalSize = 0;

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                if (file.getFileName().toString().endsWith(".java")) {
                    count++;
                    totalSize += attrs.size();
                    System.out.println("見つかった .java ファイル: " + file);
                }
                return FileVisitResult.CONTINUE;
            }
        }

        JavaFileVisitor visitor = new JavaFileVisitor();
        Files.walkFileTree(start, visitor);

        System.out.println("合計 .java ファイル数: " + visitor.count);
        System.out.println("合計サイズ: " + visitor.totalSize + " バイト");
    }
}

9. FileVisitor 使用時のよくあるミス

エラー No. 1: 中身のファイルを削除する前にディレクトリを削除しようとする。 Files.delete(dir)preVisitDirectory で呼ぶと例外になります。まず全ファイルとサブフォルダを削除し、その後でディレクトリ本体を削除してください(postVisitDirectory で行います)。

エラー No. 2: アクセスエラーの処理を忘れる。 visitFileFailed をオーバーライドしないと、保護されたファイルに遭遇した際にプログラムが予期せず終了することがあります。エラーを明示的に出力し、走査を継続するほうがよいでしょう。

エラー No. 3: 「ファイルを隠す」操作がすべての OS で同じだと期待する。 Linux と macOS では、ドット(.gitignore)で始まるファイルが隠しファイルと見なされます。一方 Windows では特別な属性が立っているかどうかで判定します。これらを混同しないでください。

エラー No. 4: マルチスレッドアプリで結果の蓄積に static フィールドを使う。 複数の走査を並行実行する場合、static フィールドは混乱のもとです。インスタンスフィールド(あるいは上の例のようなローカルクラスのフィールド)を使いましょう。

エラー No. 5: FileVisitor 内でストリームを扱う際にリソースを閉じ忘れる。 FileVisitor がファイルの読み書きを行う場合は、try-with-resources を用いてリークを防いでください。

1
タスク
JAVA 25 SELF, レベル 39, レッスン 3
ロック未解除
研究者アシスタント:プロジェクトのメモ収集
研究者アシスタント:プロジェクトのメモ収集
1
タスク
JAVA 25 SELF, レベル 39, レッスン 3
ロック未解除
デジタルクリーナー: プロジェクトの完全削除
デジタルクリーナー: プロジェクトの完全削除
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION