再帰削除は「作業領域を丸ごと片付ける」ための技
業務システムでは、「ジョブごとに作った作業ディレクトリを最後に丸ごと消したい」「テストで作った一式を一発で片付けたい」といった、“ディレクトリ配下を全部削除したい”場面がよく出てきます。
これが「再帰削除」です。
単純に 1 ファイルだけ消すのと違って、再帰削除は「中身を全部たどる」「削除順序を間違えない」「消しちゃいけない場所を消さない」という点がとても重要になります。
だからこそ、ユーティリティとしてきちんと作っておくと、安全で読みやすいコードになります。
基本の考え方「親より先に子を消す」
再帰削除の一番大事なポイントは、「親ディレクトリより先に中身(子)を消す」という順序です。
空でないディレクトリは削除できないので、必ず「ファイル → 子ディレクトリ → 親ディレクトリ」という順番で消す必要があります。
Java 7 以降なら、Files.walk を使うと「ディレクトリ配下のすべてのパス」を簡単に列挙できます。
あとはそれを「深い順(パスが長い順)」に並べ替えてから、順番に Files.delete していけば、きれいに再帰削除ができます。
実務で使える再帰削除ユーティリティの最小形
DirectoryDeletes.deleteTree の実装例
まずは、シンプルで実務でもそのまま使えるレベルのユーティリティを書いてみます。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.stream.Stream;
public final class DirectoryDeletes {
private DirectoryDeletes() {}
public static void deleteTree(Path root) throws IOException {
if (root == null) {
return;
}
if (!Files.exists(root)) {
return;
}
try (Stream<Path> stream = Files.walk(root)) {
stream
.sorted(Comparator.reverseOrder()) // 深いパスから先に
.forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
throw new RuntimeException("再帰削除中にエラー: " + path, e);
}
});
}
}
}
Javaここで重要なポイントを順番にかみ砕きます。
一つ目は Files.walk(root) です。
これは root を起点に、配下のファイルやディレクトリをすべて列挙してくれるメソッドです。
返ってくるのは Stream<Path> なので、try-with-resources で閉じてあげる必要があります。
二つ目は sorted(Comparator.reverseOrder()) です。Files.walk は通常「親 → 子」の順でパスを返しますが、その順番のまま削除しようとすると「中身があるディレクトリを先に消そうとして失敗」します。
そこで、逆順にソートすることで、「一番深いファイルやディレクトリから順に」削除できるようにしています。
これが「親より先に子を消す」をコードで表現している部分です。
三つ目は Files.deleteIfExists(path) を使っていることです。
存在しないパスが混ざっていても例外にならず、「あれば消す、なければ何もしない」という挙動になります。
再帰削除では「途中で誰かが消した」などの揺らぎもあり得るので、deleteIfExists のほうが扱いやすいです。
例題:ジョブ専用の作業ディレクトリを丸ごと片付ける
ジョブのライフサイクルと作業ディレクトリのライフサイクルを揃える
よくあるパターンとして、「ジョブごとに一時ディレクトリを作り、その中でファイルをやりくりする」という設計があります。
このとき、「ジョブが終わったらディレクトリごと消す」ようにしておくと、長期的にゴミが溜まりません。
コード例です。
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 {
Path csv = workDir.resolve("data.csv");
Files.writeString(csv, "id,name\n1,foo\n2,bar\n");
// ここで csv を使った処理をいろいろ行う
} finally {
DirectoryDeletes.deleteTree(workDir);
}
}
}
Javaここでのポイントは、「作業ディレクトリの寿命を try-finally で囲んでいる」ことです。
ジョブが成功しても失敗しても、finally で必ず deleteTree が呼ばれ、作業ディレクトリが丸ごと片付けられます。
「作ったら必ず消す」をコードとして表現しておくと、後から見ても意図が明確です。
例外処理をどう設計するかが“実務っぽさ”の肝
「削除失敗でジョブを落とすかどうか」を決める
再帰削除中に例外が起きたとき、どう扱うかは業務要件によって変わります。
例えば、「作業ディレクトリの削除に失敗しても、ジョブの結果自体は有効」という場合は、削除失敗をログに出すだけにして、ジョブは成功扱いにしたいかもしれません。
逆に、「機密情報を含むディレクトリなので、消せなかったら異常」としたい場合は、例外をそのまま上に投げてジョブを失敗扱いにするほうが安全です。
先ほどの DirectoryDeletes.deleteTree は、内部で IOException を RuntimeException に包んで投げています。
これは「削除失敗を“異常”として扱う」設計です。
もし「削除失敗でジョブを落としたくない」なら、呼び出し側で握りつぶすか、別の safe 版ユーティリティを用意します。
public static void safeDeleteTree(Path root) {
try {
deleteTree(root);
} catch (IOException | RuntimeException e) {
System.err.println("再帰削除に失敗しました: " + root + " : " + e.getMessage());
}
}
Javaここで大事なのは、「削除失敗をどう扱うか」を曖昧にせず、メソッド名や呼び出し側のコードでハッキリさせることです。
再帰削除の“危険さ”をちゃんと意識する
「消しすぎ」が一番怖い
再帰削除ユーティリティは、とても強力です。
間違ったパスを渡すと、「消してはいけないものまで一気に消す」という最悪の事故につながります。
だからこそ、次のようなことを意識して設計すると安全度が上がります。
削除対象のルートディレクトリを、アプリ専用の領域に限定する
ユーザー入力から直接パスを組み立てて deleteTree に渡さない
ログに「どのディレクトリを消そうとしたか」を必ず出す
例えば、「アプリ専用の work ディレクトリ配下しか再帰削除しない」というルールを決めておけば、
誤ってシステム全体の重要ディレクトリを消してしまうリスクを大きく減らせます。
「本当に今消していいか」を一度立ち止まる
再帰削除を呼ぶ前に、「このディレクトリは、もう誰も使っていないか」「ログや成果物として残しておく必要はないか」を一度考える癖も大事です。
特に本番環境では、「すぐ消さずに一定期間だけ残しておく」ほうが、障害調査に役立つことも多いです。
その場合は、「古い日付のディレクトリだけを再帰削除するバッチ」を別途用意するなど、
「いつ消すか」の設計も含めて考えると、より実務的な運用になります。
まとめ:再帰削除ユーティリティで身につけるべき感覚
再帰削除は、「ディレクトリ配下を全部消す」という強力な操作を、安全に、意図を持って行うための技です。
押さえておきたいのは、Files.walk と逆順ソートで「ファイル → 子ディレクトリ → 親ディレクトリ」の順に削除すること。
ジョブ専用の作業ディレクトリなど、「ライフサイクルがはっきりしている領域」に対して使うこと。
削除失敗を「異常」とみなすか「警告」とみなすかを決め、メソッドや呼び出し側で方針を明示すること。
そして何より、「どこを消すのか」「本当に今消してよいのか」を、ユーティリティ任せにせず設計として考えることです。
