CodeGym /コース /JAVA 25 SELF /ファイルに対する一括操作

ファイルに対する一括操作

JAVA 25 SELF
レベル 40, レッスン 2
使用可能

1. ディレクトリ内のファイルを列挙する

ファイル一覧の取得: Files.list と Files.walk

Java では、フォルダの内容を扱うために非常に便利な 2 つのメソッドがあります。

  • Files.list(Path) — 指定したディレクトリの最上位にあるすべてのファイルとフォルダのストリーム (Stream<Path>) を返します。
  • Files.walk(Path) — ディレクトリ内およびそのすべてのサブフォルダ内のすべてのファイルとフォルダのストリーム(再帰的)を返します。

例: ディレクトリ data にあるすべてのファイルとフォルダを出力

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

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

        // 最上位のファイルとフォルダのストリームを取得する
        try (var stream = Files.list(dir)) {
            stream.forEach(System.out::println);
        }
    }
}

すべての入れ子フォルダを(再帰的に)たどりたい場合は、Files.walk を使います:

try (var stream = Files.walk(dir)) {
    stream.forEach(System.out::println);
}

注意:
Files.walk は、構造が大きい場合に非常に多くのファイルを返すことがあります — ドライブのルートで実行すると、思わず「全宇宙の一覧」を見るはめになるかもしれません!

イメージ: list と walk の違い

メソッド 戻り値
Files.list
現在のフォルダの内容のみ
Files.walk
入れ子を含むすべてのファイルとフォルダ

2. ファイルのフィルタリング

Stream<Path> を使う大きな利点の一つは、拡張子、名前、サイズ、作成日時など、任意の条件でファイルをフィルタリングできることです。

拡張子でフィルタリング

たとえば、.txtファイルだけに絞り込みます:

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".txt"))
        .forEach(System.out::println);
}

名前でフィルタリング

名前が "report" で始まるファイルを取得します:

stream
    .filter(path -> path.getFileName().toString().startsWith("report"))
    .forEach(System.out::println);

サイズでフィルタリング

重いファイル(1 MB を超えるもの)だけを取得します:

stream
    .filter(path -> {
        try {
            return Files.size(path) > 1_000_000;
        } catch (IOException e) {
            return false;
        }
    })
    .forEach(System.out::println);

日付でフィルタリング

例: 過去 7 日以内に更新されたファイル:

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

stream
    .filter(path -> {
        try {
            FileTime lastModified = Files.getLastModifiedTime(path);
            return lastModified.toInstant().isAfter(Instant.now().minus(Duration.ofDays(7)));
        } catch (IOException e) {
            return false;
        }
    })
    .forEach(System.out::println);

組み合わせ: 複合フィルタリング

フィルタは「お好み定食」のように組み合わせられます。例えば、.log ファイルで、1 か月より古くて 10 KB を超えるもの:

stream
    .filter(path -> path.toString().endsWith(".log"))
    .filter(path -> {
        try {
            return Files.size(path) > 10_000;
        } catch (IOException e) {
            return false;
        }
    })
    .filter(path -> {
        try {
            FileTime lastModified = Files.getLastModifiedTime(path);
            return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(30)));
        } catch (IOException e) {
            return false;
        }
    })
    .forEach(System.out::println);

3. ファイルの一括コピーと削除

一括処理はシンプルです。ファイルのストリームを取得し、各要素に必要な関数を適用するだけです。重要なのは、(ファイルが使用中、すでに存在する等の)エラーを忘れずに考慮することです。

あるフォルダから別のフォルダへすべてのファイルをコピー

import java.nio.file.StandardCopyOption;

Path sourceDir = Paths.get("data");
Path targetDir = Paths.get("backup");

try (var stream = Files.list(sourceDir)) {
    stream.forEach(path -> {
        Path targetPath = targetDir.resolve(path.getFileName());
        try {
            Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
            System.out.println("コピーしました: " + path.getFileName());
        } catch (IOException e) {
            System.out.println("コピーエラー " + path.getFileName() + ": " + e.getMessage());
        }
    });
}

一時ファイル (*.tmp) をすべて削除

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".tmp"))
        .forEach(path -> {
            try {
                Files.delete(path);
                System.out.println("削除しました: " + path.getFileName());
            } catch (IOException e) {
                System.out.println("削除エラー " + path.getFileName() + ": " + e.getMessage());
            }
        });
}

Stream API を使ってパスのコレクションを処理する

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".txt"))
        .sorted((p1, p2) -> {
            try {
                return Long.compare(Files.size(p2), Files.size(p1)); // サイズで降順
            } catch (IOException e) {
                return 0;
            }
        })
        .limit(5) // 大きいもの上位 5 件のみ
        .forEach(System.out::println);
}

4. 実践タスク

指定した日時より古いファイルの検索と削除

1 年以上更新されていないファイルをすべて削除します:

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> {
            try {
                FileTime lastModified = Files.getLastModifiedTime(path);
                return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(365)));
            } catch (IOException e) {
                return false;
            }
        })
        .forEach(path -> {
            try {
                Files.delete(path);
                System.out.println("削除しました: " + path.getFileName());
            } catch (IOException e) {
                System.out.println("削除エラー " + path.getFileName() + ": " + e.getMessage());
            }
        });
}

パターンに基づくファイルの一括リネーム

たとえば、各 .txt ファイルに接頭辞 "old_" を付けます:

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".txt"))
        .forEach(path -> {
            Path newPath = path.resolveSibling("old_" + path.getFileName());
            try {
                Files.move(path, newPath);
                System.out.println("リネーム: " + path.getFileName() + " -> " + newPath.getFileName());
            } catch (IOException e) {
                System.out.println("リネームエラー " + path.getFileName() + ": " + e.getMessage());
            }
        });
}

すべてのサブフォルダからすべてのファイルをコピー(再帰的)

Files.walk を使って、すべてのサブフォルダ内のファイルを列挙します:

try (var stream = Files.walk(sourceDir)) {
    stream
        .filter(Files::isRegularFile)
        .forEach(path -> {
            Path relative = sourceDir.relativize(path);
            Path targetPath = targetDir.resolve(relative);
            try {
                Files.createDirectories(targetPath.getParent());
                Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
                System.out.println("コピーしました: " + path + " -> " + targetPath);
            } catch (IOException e) {
                System.out.println("コピーエラー " + path + ": " + e.getMessage());
            }
        });
}

5. 重要なポイントと注意点

一括処理におけるエラー処理

一括処理では、アクセス権がない、ファイルが使用中、パスが長すぎる、既に存在している等の「問題児」ファイルにほぼ必ず遭遇します。ループ内で例外を処理しないと、最初の失敗でプログラムが「落ちて」残りが処理されません。したがって、forEach の内部で必ず try-catch を使い、1 件のエラーは飲み込んで残りを処理しましょう。

再帰がパフォーマンスとコールスタックに与える影響

大きなディレクトリに対して Files.walk を使うと、手動で再帰を行う場合には非常に長い走査になることがあります。幸い、このメソッド自体は効率的に実装されており、スタックオーバーフローは発生しません。しかし、ディレクトリの削除/コピーのために独自の再帰関数を書く場合は注意してください。巨大で深い構造ではスタックが破裂することがあります。

大きなフォルダを扱う際の注意

  • Files.walk は注意して使いましょう。フォルダに数千のファイルがあると、時間がかかりメモリも多く消費します。
  • 上位レベルだけを処理したい場合は Files.list を使います。
  • マスクによる検索には名前ベースのフィルタ(endsWith、matches など)を使いましょう。

例: 「フォルダから一時ファイルを削除する」

Path tempDir = Paths.get("data/tmp");

try (var stream = Files.walk(tempDir)) {
    stream
        .filter(Files::isRegularFile)
        .filter(path -> path.toString().endsWith(".tmp"))
        .forEach(path -> {
            try {
                Files.delete(path);
                System.out.println("削除しました: " + path);
            } catch (IOException e) {
                System.out.println("削除エラー " + path + ": " + e.getMessage());
            }
        });
}

一括操作の主要メソッド

操作 メソッド/アプローチ 注意点
すべてのファイルを取得
Files.list, Files.walk
walk — 再帰、list — 最上位のみ
拡張子でフィルタリング
filter(path -> ...)
他のフィルタと組み合わせ可能
ファイル群のコピー
forEach + Files.copy
宛先フォルダの存在を確認する
ファイル群の削除
forEach + Files.delete
各操作で try-catch を使うのが望ましい
一括リネーム
forEach + Files.move
リネームは新しい名前での move と同じ

6. 一括処理でよくあるミス

エラー1: forEach 内の例外を処理しない。
とてもよくある問題です。各操作を try-catch で包まないと、最初の問題ファイルでプログラムが「落ちて」、残りは処理されません。

エラー2: フォルダをファイルとして(またはその逆に)削除/コピーしようとする。
Files.delete と Files.copy は、ファイルとフォルダでの挙動が異なります。混同しないでください。例えば、中身のあるフォルダを標準のメソッドで削除しようとするとエラーになります。

エラー3: コピー時の出力パスの組み立てミス。
構造を考慮せずに宛先パスを組み立てると(例えば relativize を使わないなど)、ファイルを上書きしたり、意図しないディレクトリ構造になったりします。

エラー4: 一度に開くファイル数が多すぎる。
ループ内で読み書き用のストリームを開く場合、クローズを忘れないでください。try-with-resources を使うのがベターです。

エラー5: 深さ制限なしの再帰走査。
非常に大きく深いフォルダに対して Files.walk を使うと、パフォーマンスやメモリで問題が発生することがあります。深さを制限する必要がある場合は、深さパラメータ付きのオーバーロードを使用します: Files.walk(dir, depth)。

エラー6: 隠し/システムファイルを暗黙に無視してしまう。
アプリがユーザーファイルを扱う場合でも、隠しファイルやシステムファイルに誤って触れてしまうことがあります。これらをフィルタリングするには、Files.isHidden(path) を使用します。

1
タスク
JAVA 25 SELF, レベル 40, レッスン 2
ロック未解除
重要なデータのバックアップ作成 💾
重要なデータのバックアップ作成 💾
1
タスク
JAVA 25 SELF, レベル 40, レッスン 2
ロック未解除
デジタル図書館員:テキストの宝物を集める 📚
デジタル図書館員:テキストの宝物を集める 📚
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION