レガシーな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.  メモリ上に全部バッファリングする
 メモリ上に全部バッファリングする
2002年にcommons-ioのFileUtilsに、初めてファイルコピーメソッドが誕生したときは、今となってはおぞましい以下のようなコードでした。
public static void fileCopy(String inFileName, String outFileName) throws
    Exception
{
    String content = FileUtils.fileRead(inFileName);
    FileUtils.fileWrite(outFileName, content);
}
今、こんなコードを書こうものならはっ倒されますね…
2.  InputStream / OutputStream
 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
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.  FileChannel (NIO)
 FileChannel (NIO)
2008年になってようやくFileChannelが使われるようになったようです。
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をループするようになっています。
4.  Files (NIO2)
 Files (NIO2)
Java7からは、NIO2になってjava.nio.file.Filesにcopyメソッドができました。なので、(ファイルコピーに関しては)commons-ioとおさらばして、Filesのcopyを直接使うことで同じ効果が期待できます。
Files.copy(source.toPath(), dest, StandardCopyOption.REPLACE_EXISTING);
ベンチマーク
さて、これらのベンチマークをとってみます。ベンチマークのコードは以下にあります。20MBのファイルをコピーを繰り返したときのスループットを比較します。
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を使おう。