Help us understand the problem. What is going on with this article?

Javaでのファイルコピー史

More than 3 years have passed since last update.

レガシーなJavaで書かれたシステムのコードを見ていると、以下のようにInputStreamでファイルを開いて、OutputStreamでコピー先のファイルに書き込むみたいなものがあったりします。

try(InputStream input = new FileInputStream(srcFile);
    OutputStream output = new FileOutputStream(dstFile)) {
    byte[] buffer = new byte[BUFFER_SIZE];
    int size = -1;
    while ((size = input.read(buffer)) > 0) {
      output.write(buffer, 0, size);
    }
}

他にはどういう方法があるのでしょうか。ファイルコピーの歴史が詰まっている、commons-ioの実装の変遷をふりかえり、それぞれの手法のベンチマークをとってみます。

1. :snail: メモリ上に全部バッファリングする

2002年にcommons-ioのFileUtilsに、初めてファイルコピーメソッドが誕生したときは、今となってはおぞましい以下のようなコードでした。

https://github.com/apache/commons-io/blob/acdb1679bbb4f0a1af1e5805b707b1ef23bcdce9/src/java/org/apache/commons/io/FileUtils.java#L234

FileUtils.java
public static void fileCopy(String inFileName, String outFileName) throws
    Exception
{
    String content = FileUtils.fileRead(inFileName);
    FileUtils.fileWrite(outFileName, content);
}

今、こんなコードを書こうものならはっ倒されますね…

2. :turtle: InputStream / OutputStream

前述の方法はあんまりなので、2003年にFileInputStreamでファイルの中身を読み、FileOutputStreamで書き込む、当時のオーソドックス実装に変わったようです。といっても、J2SE1.4は2002年リリースなので、後述のFileChannelが使えた時代ではあるのですが。

https://github.com/apache/commons-io/blob/d9d353082503a217fa6c6510622973d018db0e26/src/java/org/apache/commons/io/FileUtils.java#L604
https://github.com/apache/commons-io/blob/d9d353082503a217fa6c6510622973d018db0e26/src/java/org/apache/commons/io/CopyUtils.java

CopyUtils.java
final byte[] buffer = new byte[bufferSize];
int count = 0;
int n = 0;
while (-1 != (n = input.read(buffer))) {
    output.write(buffer, 0, n);
    count += n;
}

3. :racehorse: FileChannel (NIO)

2008年になってようやくFileChannelが使われるようになったようです。

https://github.com/apache/commons-io/blob/7bd2c82a6721c57f49f8caec8a94171cd5a2f8c7/src/java/org/apache/commons/io/FileUtils.java#L665

FileUtils.java
private static void doCopyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException {
    if (destFile.exists() && destFile.isDirectory()) {
        throw new IOException("Destination '" + destFile + "' exists but is a directory");
    }

    FileChannel input = new FileInputStream(srcFile).getChannel();
    try {
        FileChannel output = new FileOutputStream(destFile).getChannel();
        try {
            output.transferFrom(input, 0, input.size());
        } finally {
            IOUtils.closeQuietly(output);
        }
     } finally {
        IOUtils.closeQuietly(input);
     }

    if (srcFile.length() != destFile.length()) {
        throw new IOException("Failed to copy full contents from '" +
            srcFile + "' to '" + destFile + "'");
    }
    if (preserveFileDate) {
        destFile.setLastModified(srcFile.lastModified());
    }
}

commons-ioの現在のtrunk実装もこれですが、Windowsでラージファイルがコピーできない(IO-175)とのことから、30MBごとにtransferFromをループするようになっています。

https://github.com/apache/commons-io/blob/trunk/src/main/java/org/apache/commons/io/FileUtils.java#L1148

4. :red_car: Files (NIO2)

Java7からは、NIO2になってjava.nio.file.Filesにcopyメソッドができました。なので、(ファイルコピーに関しては)commons-ioとおさらばして、Filesのcopyを直接使うことで同じ効果が期待できます。

Files.copy(source.toPath(), dest, StandardCopyOption.REPLACE_EXISTING);

ベンチマーク

さて、これらのベンチマークをとってみます。ベンチマークのコードは以下にあります。20MBのファイルをコピーを繰り返したときのスループットを比較します。

https://github.com/kawasima/copyfile-benchmark

Linux

Linuxの以下のようなディスク性能のマシンで、

% sync; time bash -c "(dd if=/dev/zero of=bf bs=8k count=500000; sync)" 
4096000000 バイト (4.1 GB) コピーされました、 7.00345 秒、 585 MB/秒

次のような結果となりました。

Benchmark                            Mode  Cnt  Score   Error  Units
InputStream2OutputStream.benchmark  thrpt    5  1.669 ± 0.055  ops/s
NIOFileChannel.benchmark            thrpt    5  1.860 ± 0.113  ops/s
NIO2Files.benchmark                 thrpt    5  7.110 ± 1.115  ops/s

Filesのコピーメソッドを使うのが、圧倒的に速い結果となります。

Filesが速い理由

Filesのコピーメソッドを辿っていくと、sun.nio.fs.UnixCopyFileで実際コピーしているようです。

    private static void copyFile(UnixPath source,
                                 UnixFileAttributes attrs,
                                 UnixPath  target,
                                 Flags flags,
                                 long addressToPollForCancel)
        throws IOException
    {
        int fi = -1;
        try {
            fi = open(source, O_RDONLY, 0);
        } catch (UnixException x) {
            x.rethrowAsIOException(source);
        }

        try {
            // open new file
            int fo = -1;
            try {
                fo = open(target,
                           (O_WRONLY |
                            O_CREAT |
                            O_EXCL),
                           attrs.mode());
            } catch (UnixException x) {
                x.rethrowAsIOException(target);
            }


            // set to true when file and attributes copied
            boolean complete = false;
            try {
                // transfer bytes to target file
                try {
                    transfer(fo, fi, addressToPollForCancel);

NIOパターンと同じく、ふつうにファイル開いてバッファコピーしてそうですが… openのところをたどると、sun.misc.Unsafeを使って、ネイティブのバッファが使われています。それでこれだけ差がついちゃうようです。

Windows

同じベンチマークをWindowsで流してみました。

Benchmark                            Mode  Cnt  Score   Error  Units
InputStream2OutputStream.benchmark  thrpt    5  1.032 ± 0.421  ops/s
NIOFileChannel.benchmark            thrpt    5  5.466 ± 9.434  ops/s
NIO2Files.benchmark                 thrpt    5  3.153 ± 2.775  ops/s

FileChannelとFiles.copyの結果が逆転します。Windowsには疎いので、ちょっと理由までは分かりませんでした…
(Files.copyでは、CopyFileExWAPIが使われており、これがオーバーヘッドかかるのでしょうか?)

まとめ

java.nio.fileのクラスは、java.ioとの互換性が弱いですが、Nativeの機能をふんだんに呼んでくれるので、性能面でメリットが大きいことが多いです。
特に巨大なファイルや多くのファイルを扱う場合は、積極的に使っていくとよいのではないでしょうか。

  • Java1.4以上は、FileChannelを使おう。
  • Java7以上は、Files.copyを使おう。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした