Java Tips | 基本ユーティリティ:ファイル削除

Java Java
スポンサーリンク

ファイル削除は「後片付けをちゃんとやる」ための技

業務システムは、動けば終わりではなく「後片付け」まで含めて仕事です。
一時ファイル、古いバックアップ、失敗した処理の中途半端な成果物など、放っておくとディスクを圧迫し、トラブルの原因になります。

だからこそ、「ファイル削除」をユーティリティとしてきちんと設計しておくと、
一時ファイルを確実に消したり、不要になった成果物を安全に片付けたりできるようになります。
ポイントは、「消すべきものだけを消す」「失敗したときにどう振る舞うかを決めておく」ことです。


基本:Files.delete と deleteIfExists の違い

delete は「なかったら例外」、deleteIfExists は「なければ何もしない」

Java 7 以降では、java.nio.file.Files を使ってファイルを削除します。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class DeleteBasic {

    public static void main(String[] args) throws IOException {
        Path path = Path.of("work/temp.txt");
        Files.delete(path);
    }
}
Java

Files.delete は、「ファイル(または空ディレクトリ)が存在していれば削除する」「存在しなければ例外を投げる」という挙動です。
「絶対にあるはずのファイルがなかったら異常」とみなしたいときに向いています。

一方、「あってもなくてもよい、一応消しておきたい」という場面では deleteIfExists を使います。

Files.deleteIfExists(path);
Java

こちらは、「存在していれば削除する」「存在しなければ何もしない(例外も出さない)」という挙動です。
一時ファイルや、掃除バッチなど、「なければないで構わない」ものに向いています。

ここでの重要ポイントは、「“存在しないこと”をエラーにしたいかどうかで、delete と deleteIfExists を使い分ける」という感覚です。


実務で使える「安全なファイル削除ユーティリティ」の最小形

「失敗してもアプリは落とさない」safeDelete

一時ファイルの削除など、「消せなかったからといって業務処理を止めたくはない」場面が多くあります。
その場合は、「削除を試みるが、失敗してもログに出すだけ」というユーティリティが便利です。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public final class FileDeletes {

    private FileDeletes() {}

    public static void safeDelete(Path path) {
        if (path == null) {
            return;
        }
        try {
            Files.deleteIfExists(path);
        } catch (IOException e) {
            System.err.println("ファイル削除に失敗しました: " + path + " : " + e.getMessage());
        }
    }
}
Java

使う側はこう書きます。

Path temp = Path.of("work/tmp-12345.dat");
try {
    // 一時ファイルを使った処理
} finally {
    FileDeletes.safeDelete(temp);
}
Java

ここで深掘りしたいのは、「try-finally と safeDelete の組み合わせで、“使ったら必ず片付ける”をコードとして表現している」ことです。
業務処理の成否に関わらず、finally で必ず削除が呼ばれ、削除に失敗してもアプリ全体は落ちません。
「後片付けはするが、片付け失敗で本体を巻き添えにしない」というバランスが実務的です。


「必ず消えていてほしい」場合のユーティリティ

消せなかったら異常として扱う requireDeleted

逆に、「このファイルが消せないなら、処理を続けると危険」というケースもあります。
例えば、「再実行時に同名ファイルがあると壊れる」「機密情報なので必ず削除されていなければならない」といった場面です。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public final class RequiredDeletes {

    private RequiredDeletes() {}

    public static void requireDeleted(Path path, String description) throws IOException {
        if (path == null) {
            return;
        }
        try {
            Files.deleteIfExists(path);
        } catch (IOException e) {
            throw new IOException("必ず削除すべきファイルを削除できませんでした: " + description + " (" + path + ")", e);
        }
    }
}
Java

使う側はこうです。

RequiredDeletes.requireDeleted(Path.of("work/secret.dat"), "機密データ一時ファイル");
Java

ここでの重要ポイントは、「“消せなかったこと”を業務的な異常として扱う」ことです。
単に IOException を投げるのではなく、「何のためのファイルか」をメッセージに含めることで、ログを見たときに状況がすぐ分かります。


ディレクトリごと削除したい場合の考え方

Files.walk を使った再帰削除

Files.delete は「ファイル」か「空ディレクトリ」しか削除できません。
中身のあるディレクトリを丸ごと消したい場合は、自分で中身をたどって削除する必要があります。

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

public final class DirectoryDeletes {

    private DirectoryDeletes() {}

    public static void deleteTree(Path dir) throws IOException {
        if (dir == null || !Files.exists(dir)) {
            return;
        }
        Files.walk(dir)
             .sorted(Comparator.reverseOrder())
             .forEach(path -> {
                 try {
                     Files.deleteIfExists(path);
                 } catch (IOException e) {
                     throw new RuntimeException("ディレクトリ削除中にエラー: " + path, e);
                 }
             });
    }
}
Java

使う側はこうです。

DirectoryDeletes.deleteTree(Path.of("work/tmp-job-12345"));
Java

ここで深掘りしたいのは、「削除順を“深い階層から先に”している」点です。
Files.walk は親→子の順にパスを返すので、そのまま削除すると「中身があるディレクトリを先に消そうとして失敗」します。
Comparator.reverseOrder() で逆順にソートすることで、「ファイル→子ディレクトリ→親ディレクトリ」の順に削除できるようにしています。


例題:ジョブごとの作業ディレクトリを丸ごと片付ける

「ジョブ開始時に作って、終了時に丸ごと消す」

バッチ処理などで、「ジョブごとに一時ディレクトリを作り、その中でファイルをやりくりする」という設計はよくあります。
その場合、「ジョブ終了時にディレクトリごと削除する」ユーティリティがあると便利です。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class JobRunner {

    public void runJob() throws IOException {
        Path workDir = Files.createTempDirectory("job-");
        try {
            // workDir 配下でいろいろなファイルを使う
            Path temp = workDir.resolve("data.csv");
            Files.writeString(temp, "...");
        } finally {
            DirectoryDeletes.deleteTree(workDir);
        }
    }
}
Java

ここでのポイントは、「“ジョブのライフサイクル”と“作業ディレクトリのライフサイクル”を一致させている」ことです。
ジョブが終われば、そのジョブ専用のディレクトリも消えるので、長期的にゴミが溜まりません。
また、問題が起きたときに「一時的に削除をコメントアウトして中身を調査する」といった運用もしやすくなります。


ファイル削除の“落とし穴”と注意点

「消したつもりが消えていない」ケース

ファイル削除は、次のような理由で失敗することがあります。

同じファイルを別プロセスや別スレッドが開きっぱなし
権限が足りない
ネットワークドライブの一時的なエラー

これらは、Files.deletedeleteIfExists が IOException を投げることでしか分かりません。
だからこそ、「safeDelete ではログに出す」「requireDeleted では例外に包んで表に出す」といったポリシーを決めておくことが大事です。

「消してはいけないものを消してしまう」リスク

ファイル削除で一番怖いのは、「本当は残しておくべきファイルを誤って消してしまう」ことです。
これを防ぐために意識したいのは次のようなことです。

削除対象のパスを組み立てるロジックをユーティリティに閉じ込める
「どのディレクトリ配下だけを消すか」を明確に決める
ユーザー入力から直接パスを作って削除しない

特に、「ディレクトリごと削除する」ユーティリティは強力なので、「このディレクトリ配下だけ」という前提をしっかり決めておくと安全です。


まとめ:ファイル削除ユーティリティで身につけるべき感覚

ファイル削除は、「ただ消す」ではなく、「何を、いつ、どのくらいの強さで消すか」を設計する行為です。

押さえておきたい感覚はこうです。
「存在しないことをエラーにしたいかどうか」で、deletedeleteIfExists を使い分ける。
一時ファイルなどは、try-finally と safeDelete で「使ったら必ず片付ける」をコードにする。
「必ず消えていてほしい」ものは requireDeleted のようなメソッドで、消せなかったら異常として扱う。
ディレクトリ削除は Files.walk と逆順ソートで、「ファイル→子ディレクトリ→親ディレクトリ」の順に消す。
「消し忘れ」と「消しすぎ」の両方を意識し、削除対象と削除ポリシーをユーティリティに集約する。

タイトルとURLをコピーしました