Java Tips | 基本ユーティリティ:ファイルコピー

Java Java
スポンサーリンク

ファイルコピーは「安全にデータを移動・退避させる」ための技

業務システムでは、「インポート用ファイルをバックアップしてから処理したい」「出力結果を別ディレクトリに退避したい」「テンプレートを所定の場所にコピーして使いたい」といった、“ファイルをコピーする”場面が頻繁に出てきます。
ここで雑にコピーすると、「上書きしてはいけないファイルを消してしまう」「途中で失敗して中途半端なファイルが残る」といった事故につながります。

だからこそ、ファイルコピーをユーティリティとして整理し、「どういうときに上書きするか」「存在していたらどうするか」「例外はどう扱うか」を決めておくと、実務でかなり効いてきます。


基本:Files.copy の使い方と「上書き」の扱い

一番シンプルなファイルコピー

Java 7 以降では、java.nio.file.Filescopy を使うのが基本です。

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

public class FileCopyBasic {

    public static void main(String[] args) throws IOException {
        Path src = Path.of("data/input.csv");
        Path dest = Path.of("backup/input.csv");

        Files.copy(src, dest);
    }
}
Java

このコードは、「data/input.csvbackup/input.csv にコピーする」という意味です。
ここで重要なのは、デフォルトでは「コピー先がすでに存在していたら例外が投げられる」という点です。

コピー先が存在しているときにどうするかは、業務的にとても重要な判断なので、
Files.copy は「何も指定しないなら“上書きしない”」という安全側の挙動になっています。

上書きしたい場合はオプションを明示する

「バックアップファイルは毎回上書きしてよい」といったケースでは、上書きオプションを明示します。

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

public class FileCopyOverwrite {

    public static void main(String[] args) throws IOException {
        Path src = Path.of("data/input.csv");
        Path dest = Path.of("backup/input.csv");

        Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);
    }
}
Java

StandardCopyOption.REPLACE_EXISTING を指定すると、「コピー先が存在していても上書きする」挙動になります。

ここで深掘りしたいポイントは、「“上書きするかどうか”をコード上で明示することが、事故防止につながる」ということです。
デフォルトの挙動に任せるのではなく、「このコピーは上書きしてよい」「このコピーは上書きしてはいけない」を、オプション指定でハッキリさせる癖をつけると安全です。


実務で使える「ファイルコピーユーティリティ」の最小形

「上書きあり」「上書きなし」をメソッド名で分ける

毎回 Files.copy にオプションを直書きすると、呼び出し側のコードが読みにくくなります。
そこで、「上書きあり」「上書きなし」をメソッドレベルで分けたユーティリティを用意します。

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

public final class FileCopier {

    private FileCopier() {}

    public static void copy(Path src, Path dest) throws IOException {
        Files.copy(src, dest);
    }

    public static void copyOverwrite(Path src, Path dest) throws IOException {
        Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);
    }
}
Java

使う側はこう書けます。

Path src  = Path.of("data/input.csv");
Path dest = Path.of("backup/input.csv");

// 上書きしたくないコピー
FileCopier.copy(src, dest);

// 上書きしてよいコピー
FileCopier.copyOverwrite(src, dest);
Java

ここでの重要ポイントは、「メソッド名に“業務的な意図”を乗せる」ことです。
copyOverwrite と書かれていれば、「ここは上書きしてよいバックアップなんだな」と一目で分かります。
Files.copy(..., REPLACE_EXISTING) があちこちに散らばるより、意図が読み取りやすくなります。


例題:インポート前にファイルをバックアップする

「処理前に元ファイルを退避しておく」パターン

よくある業務バッチとして、「in/ ディレクトリに置かれた CSV を読み込み、処理後に backup/ に退避する」という処理を考えます。
ここでは、「バックアップは上書きしてよい」とします。

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

public class ImportJob {

    public void run() throws IOException {
        Path src  = Path.of("in/users.csv");
        Path backup = Path.of("backup/users.csv");

        // まずバックアップを取る
        FileCopier.copyOverwrite(src, backup);

        // ここから先は src を読み込んで処理する
        importUsers(src);
    }

    private void importUsers(Path csv) {
        // CSV 読み込み処理…
    }
}
Java

ここで深掘りしたいのは、「“バックアップを取る”という業務的な意味を、ファイルコピーの前に明示している」ことです。
単に Files.copy と書くのではなく、「なぜコピーしているのか」「上書きしてよいのか」をコードから読み取れるようにしておくと、
後から読む人が安心して変更できます。


ディレクトリごとコピーしたい場合の考え方

Files.walk を使って再帰コピーする

Files.copy は「ファイル単体」か「空のディレクトリ」しかコピーしてくれません。
「ディレクトリ配下を丸ごとコピーしたい」場合は、自分で再帰的にたどる必要があります。

シンプルなディレクトリコピーのユーティリティ例です。

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

public final class DirectoryCopier {

    private DirectoryCopier() {}

    public static void copyTree(Path srcDir, Path destDir) throws IOException {
        Files.walk(srcDir).forEach(src -> {
            try {
                Path relative = srcDir.relativize(src);
                Path dest = destDir.resolve(relative);

                if (Files.isDirectory(src)) {
                    Files.createDirectories(dest);
                } else {
                    Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);
                }
            } catch (IOException e) {
                throw new RuntimeException("ディレクトリコピー中にエラー: " + src, e);
            }
        });
    }
}
Java

使う側はこうです。

DirectoryCopier.copyTree(Path.of("templates"), Path.of("work/templates"));
Java

ここでの重要ポイントは、「ディレクトリコピーは“ディレクトリ作成”と“ファイルコピー”の組み合わせである」と理解することです。
Files.walk で元ディレクトリをたどり、ディレクトリなら createDirectories、ファイルなら copy、という分担にすると、
単体のファイルコピーユーティリティともきれいに組み合わせられます。


ファイルコピーの“落とし穴”と注意点

コピー元・コピー先の存在と権限

コピー前に、「コピー元が本当に“読み取り可能なファイル”か」「コピー先ディレクトリが存在していて書き込み可能か」を確認しておくと、
エラーを早めに検知できます。

import java.nio.file.Files;

public final class SafeFileCopier {

    private SafeFileCopier() {}

    public static void copyReadableFile(Path src, Path dest) throws IOException {
        if (!Files.isRegularFile(src) || !Files.isReadable(src)) {
            throw new IllegalArgumentException("コピー元が読み取り可能なファイルではありません: " + src);
        }
        Files.createDirectories(dest.getParent());
        Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);
    }
}
Java

ここでのポイントは、「“前提条件”をコピー前にチェックして、以降の処理をシンプルにする」ことです。
「そもそもファイルじゃなかった」「ディレクトリがなかった」という問題を、コピー中の例外に任せるのではなく、
業務的に意味のあるメッセージで表に出すと、運用・調査が楽になります。

大きなファイルと I/O コスト

大きなファイルをコピーするときは、単純に Files.copy を呼ぶだけでも時間がかかります。
バッチ処理の中で大量のファイルをコピーする場合は、「どのタイミングでコピーするか」「並列にやりすぎないか」も設計ポイントになります。

ここで大事なのは、「ファイルコピーは“ただのユーティリティ呼び出し”ではなく、“重い I/O 処理”である」という意識です。
ログに進捗を出したり、スレッドプールを分けたりして、「コピーがボトルネックになっていないか」を意識できると、実務レベルの設計に近づきます。


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

ファイルコピーは、「ただデータを複製する」だけではなく、「何のためにコピーし、上書きしてよいのか、前提条件は何か」をコードで表現する行為です。

押さえておきたい感覚はこうです。

Files.copy のデフォルトは「コピー先が存在したら例外」であり、上書きしたいときは REPLACE_EXISTING を明示する。
「上書きあり/なし」をユーティリティメソッドに分け、メソッド名で意図を伝える。
インポート前のバックアップなど、「なぜコピーしているか」をコメントやメソッド名で表現する。
ディレクトリコピーは「ディレクトリ作成+ファイルコピー」の組み合わせとして設計する。
コピー元・先の前提条件(存在・権限)をチェックし、エラーは業務的に意味のあるメッセージで表に出す。

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