乐趣区

java多种文件复制方式以及效率比较

1. 背景

java 复制文件的方式其实有很多种, 可以分为

  • 传统的字节流读写复制 FileInputStream,FileOutputStream,BufferedInputStream,BufferedOutputStream
  • 传统的字符流读写复制 FileReader,FileWriter,BufferWriter,BufferedWriter,BufferedReader
  • NIO 系列的 FileChannel
  • FileChannel+ 缓冲
  • java.nio.Files.copy()
  • 第三方包中的 FileUtils.copy 方法, 比如 org.apache.commons.io.FileUtils,org.codehaus.plexus.util.FileUtils 等等.

所以呢, 看看各种方法效率怎么样, 主要衡量的标准就是时间, 另外的一些标准包括大文件的复制时的内存溢出等问题.

2. 概述

由于很多时候复制文件都包括了文件夹下的所有子目录及文件的复制, 所以作者采用的遍历 + 复制方法去复制文件. 就是把整个复制过程分为先遍历, 遍历的过程中遇到文件夹就创建, 遇到文件就调用不同的复制方法.
遍历的 5 种方法:

  • (1)File.listFiles()
  • (2)File.list()
  • (3)org.codehaus.plexus.util.FileUtils.getFiles()
  • (4)org.apache.commons.io.FileUtils.listFiles()
  • (5)java nio 中的 java.nio.file.Files.walkFileTree

复制的 8 种方法:

  • (1)FileInputStream+FileOutputStream
  • (2)BufferedInputStream+BufferedOutputStream
  • (3)FileReader+FileWriter
  • (4)BufferedReader+BufferedWriter
  • (5)FileChannel
  • (6)FileChannel+buffer
  • (7)org.apache.commons.io.FileUtils.copyFile()
  • (8)java.nio.file.Files.copy()

另外作者不太想看控制台 ….. 所以配合了一点 swing 使用.

3.jar 包

1.org.apache.commons
2.org.codehaus.plexus

4. 遍历

(1)listFiles()

 private static void traverseByListFiles(File srcFile,File desFile) throws IOException
{if(srcFile.isDirectory())
    {File[] files = srcFile.listFiles();
        assert files != null;
        for(File file : files)
        {File desFileOrDir = new File(desFile.getAbsolutePath() + File.separator + file.getName());
            if(file.isDirectory())
            {if(desFileOrDir.exists())
                    desFileOrDir.delete();
                desFileOrDir.mkdirs();}
            traverseByListFiles(file, desFileOrDir);
        }
    }
    else 
    {copyFile(srcFile, desFile);
    }
}

通过 srcFile 的 listFiles()获取所有的子文件与子文件夹, 然后判断是否是目录
如果是目录, 首先判断有没有这个文件 (有时候本来是文件夹但是却存在同名的文件, 就先删除), 再创建文件夹, 然后递归执行函数.
如果不是目录, 直接把两个 File 作为参数进行文件复制, 里面用什么方法后面会设置.

(2)list()

private static void traverseByList(File srcFile,File desFile) throws IOException
{if (srcFile.isDirectory())
    {String[] files = srcFile.list();
        assert files != null;
        for (String file : files)
        {File subSrcFile = new File(srcFile, file);
            File subDesFile = new File(desFile, file);
            if (subSrcFile.isDirectory())
            {if (subDesFile.exists())
                    subDesFile.delete();
                subDesFile.mkdirs();}
            traverseByList(subSrcFile, subDesFile);
        }
    }
    else
    {copyFile(srcFile, desFile);
    }
}

list 与第一种 listFiles()类似, 不过是 String[], 也是先判断目录, 创建目录, 不是目录直接复制

(3)org.codehaus.plexus.util.FileUtils.getFiles

private static void traverseByGetFiles(File srcFile, File desFile) throws IOException
{if (srcFile.isDirectory())
    {java.util.List<File> fileList = org.codehaus.plexus.util.FileUtils.getFiles(srcFile,null,null);
        for (File file : fileList)
        {File desFileOrDir = new File(desFile.getAbsolutePath() + File.separator + file.getName());
            if(file.isDirectory())
            {if(desFileOrDir.exists())
                    desFileOrDir.delete();
                desFileOrDir.mkdirs();}
            traverseByListFiles(file, desFileOrDir);
        }
    }
    else
    {copyFile(srcFile, desFile);
    }
}

这是用了别人的工具类进行遍历.

org.codehaus.plexus.util.FileUtils.getFiles(srcFile,null,null);

返回的结果的 java.util.List

(4)Commons.io 工具包

private static void traverseByCommonsIO(File srcFile, File desFile) throws IOException
{if (srcFile.isDirectory())
    {Collection<File> files = org.apache.commons.io.FileUtils.listFiles(srcFile,null,false);
        for (File file : files)
        {File desFileOrDir = new File(desFile.getAbsolutePath() + File.separator + file.getName());
            if(file.isDirectory())
            {if(desFileOrDir.exists())
                    desFileOrDir.delete();
                desFileOrDir.mkdirs();}
            traverseByCommonsIO(file, desFileOrDir);
        }
    }
    else 
    {copyFile(srcFile, desFile);
    }
}

使用 org.apache.commons.io.FileUtils 的 listFiles 方法,参数为要遍历的目录,一个 null 和一个 false,第二个参数表示过滤器,表示过滤出特定后缀名的文件,类型为 String [], 第三个布尔参数表示是否递归访问子目录.

(5)NIO–walkFileTree

利用 FileVisitor 这个接口. 实际中常用 SimpleFileVisitor.

private static void traverseByNIO2(File srcFile) throws IOException
{java.nio.file.Files.walkFileTree(srcFile.toPath(), new SimpleFileVisitor<>() {
        @Override
        public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException 
        {File d = new File(des.toString() + path.toAbsolutePath().toString().substring(src.toString().length()));
            new File(d.toString().substring(0, d.toString().lastIndexOf(File.separator))).mkdirs();
            copyFile(path.toFile(), d);
            return FileVisitResult.CONTINUE;
        }
    });
}

FileVisitor 接口定义了四个方法,分别为:

public interface FileVisitor<T>
{FileVisitResult preVisitDirectory(T dir,BasicFileAttributes attrs)
    {// 访问 dir 前的操作,dir 类型一般为 java.nio.Path}
    
    FileVisitResult postVisitDirectory(T dir,BasicFileAttributes attrs)
    {// 访问 dir 后的操作}
    
    FileVisitResult visitFile(T file,BasicFileAttributes attrs)
    {// 访问 file 时的操作}
    
    FileVisitResult visitFileFailed(T file,BasicFileAttributes attrs)
    {// 访问 file 失败时的操作}
}

在上面的例子中只是实现了 visitFile,因为只是复制操作,首先判断是否是源目录的路径,不是的话创建文件夹再复制文件.
这里说一下返回值 FileVisitResult.FileVisitResult 是一个枚举类型,根据返回值判断是否继续遍历.
FileVisitResult 可取值:

  • CONTINUE: 继续
  • TERMINNATE: 结束
  • SKIP_SIBLINGS: 继续,跳过同一目录的节点
  • SKIP_SUBTREE: 继续,跳过子目录,但会访问子文件

5. 复制

(1)FileInputStream+FileOutputStream

首先是经典的字节流 FileInputStream+FileOutputStream, 这个比较简单, 使用 FileInputStream 读取后使用 FileOutputStream 写入, 不过效率嘛 ….. 一般般.

private static void copyByFileStream(File srcFile,File desFile) throws IOException
{FileInputStream inputStream = new FileInputStream(srcFile);
    FileOutputStream outputStream = new FileOutputStream(desFile);
    byte [] b = new byte[1024];
    while(inputStream.read(b) != -1)
    {outputStream.write(b);
        addCopySize();}
    inputStream.close();
    outputStream.close();}

这里说一下三个 read 方法的区别,FileInputStream 有三个 read 方法:

input.read();
input.read(b);
input.read(b,off,len);

A.read()

逐个字节进行读取, 返回 int, 写入时直接使用 write(n);

int n = input.read();
output.write(n);

这个可以说是三个 read 中最慢的 …. 作者试了一个 2G 左右的文件, 用了大概 10 分钟才复制 160M……

B.read(b)

参数是一个 byte [], 将字节缓冲到其中, 返回数组的字节个数, 这个比 read()快很多.

byte [] b = new byte[1024];
while(input.read(b) != -1)
    output.write(b);

C.read(b,off,len)

这个方法其实和 read(b)差不多,read(b)相当于省略了参数的 read(b,off,len).

byte [] b = new byte[1024];
int n;
while((n = input.read(b,0,1024))!=-1)
    output.write(b,0,n);
public int read(byte b[], int off, int len) throws IOException 
{return readBytes(b, off, len);
}

public int read(byte b[]) throws IOException 
{return readBytes(b, 0, b.length);
}

这两个都是调用一样的 readBytes():

private native int readBytes(byte b[], int off, int len) throws IOException;

至于效率 … 可以看看结果(作者用的是 10G 内的小文件):

可以看到, 没有哪个一定比另外一个更快 (不过最后一个误差有点太大了?7G 不够的文件.).
采用哪一个建议自己去测试, 毕竟这存在很多误差, 比如文件,java 版本, 机器本身等等, 仅供参考.

(2)BufferedInputStream+BufferedOutputStream

缓冲字节流 BufferedInputStream+BufferedOutputStream, 相比起 FileInputStream,BufferedInputStream 读取时会先从缓冲区读取数据, 缓冲区无可读数据再从文件读取, 所以会比 FileInputStream 快.

private static void copyByBufferStream(File srcFile,File desFile) throws IOException
{BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(srcFile));
    BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(desFile));
    byte [] b = new byte[1024];
    while(inputStream.read(b) != -1)
    {addCopySize();
        outputStream.write(b);
    }
    inputStream.close();
    outputStream.close();}

这里也说一下 BufferedInputStream 的三个 read(实际上还有, 还有 readN, 与 read(),read()肯定最慢,readN 作者很少用, 所以就没列出来了)

read(b);
read(b,off,len);
readAllBytes();

A.read(b)

这个其实和 FileInputStream 的那个没啥区别, 把一个字节数组仍进去就好了.

B.read(b,off,len)

这个 …. 也和 FileInputStream 那个没啥区别, 不说了

C.readAllBytes()

这个一次可以读取所有的字节. 不过用这个虽然省事, 可以直接

output.write(input.readAllBytes());

但是呢, 有代价的:

会出现 OutOfMemory 错误, 就是对于大文件还是老老实实分开吧, 不要 ” 一口搞定 ”,” 多吃几口 ”.

看看效率:

readAllBytes 对于大文件 (作者这个是 5G 内的文件) 直接爆内存 ….

readAllBytes()又爆了 ….. 这个才 2G 不到的文件 …readAllBytes()看来不是很给力啊 …. 不过对于小文件效率还可以接受.

(3)FileReader+FileWriter

字符流读写 FileReader+FileWriter, 相比起字节流的 read, 基本上把 byte[]换成 char[]即可, 因为是逐个字符读取, 而字节流是逐个字节读取因此采用 byte[].
注意这个不能用来读取图片, 音乐等文件, 不然复制出来的文件打不开.

private static void copyByFileReader(File srcFile,File desFile) throws IOException
{FileReader reader = new FileReader(srcFile);
    FileWriter writer = new FileWriter(desFile);

    char [] c = new char[1024];
    while(reader.read(c) != -1)
    {addCopySize();
        writer.write(c);
    }
    reader.close();
    writer.close();}

(4)BufferedReader+BufferedWriter

缓冲字符流读写 BufferedReader+BufferedWriter,BufferedReader 相比起 FileReader 有一个 readLine()方法, 可以每行读入, 会比 FileReader 快. 对应的 BufferedWriter 提供了 write(String)方法, 当然也有 write(String s,int off,int len). 同样这个不能用来读取图片等.

private static void copyByBufferReader(File srcFile,File desFile) throws IOException
{BufferedReader reader = new BufferedReader(new FileReader(srcFile));
    BufferedWriter writer = new BufferedWriter(new FileWriter(desFile));

    char [] c = new char[1024];
    while(reader.read(c) != -1)
    {addCopySize();
        writer.write(c);
    }
    reader.close();
    writer.close();}

(5)NIO–FileChannel

通过 FileChannel 复制, 首先通过 FileInputStream 与 FileOutputStream 打开流, 再用 getChannel()方法. 最后使用 transferTo()或 transferFrom()进行复制, 一条语句即可, 十分方便, 而且效率很高.

private static void copyByFileChannel(File srcFile,File desFile) throws IOException
{FileChannel srcChannel = new FileInputStream(srcFile).getChannel();
    FileChannel desChannel = new FileOutputStream(desFile).getChannel();
    srcChannel.transferTo(0,srcChannel.size(),desChannel);
    srcChannel.close();
    desChannel.close();}

(6)NIO–FileChannel+ByteBuffer

在利用了 FileInputStream 与 FileOutputStream 打开了 FileChannel 的基础上, 配合 ByteBuffer 使用.

private static void copyByFileChannelWithBuffer(File srcFile,File desFile) throws IOException
{FileChannel srcChannel = new FileInputStream(srcFile).getChannel();
    FileChannel desChannel = new FileOutputStream(desFile).getChannel();
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    while(srcChannel.read(buffer) != -1)
    {buffer.flip();
        desChannel.write(buffer);
        buffer.clear();
        addCopySize();}
    srcChannel.close();
    desChannel.close();}

flip 的意思是 ” 翻转 ”,

buffer.flip();

把 Buffer 从写模式变为读模式, 接着 write(buffer), 再把 buffer 清空.

看看这两种方法效率:

另外作者发现 transferTo 的 ” 上限 ” 为 2G, 就是对于大于 2G 的单个文件最多最能复制 2 个 G.

所以 … 对于大文件没有可比性了.

(7)FileUtils.copyFile()

这是工具类, 没啥好说的, 参数是两个 File, 分别表示源与目标.

private static void copyByCommonsIO(File srcFile,File desFile) throws IOException
{FileUtils.copyFile(srcFile, desFile);
}

(8)Files.copy()

这是官方提供的 Files 工具类, 前两个参数为 Path, 分别表示源与目标, 可以设置第三个参数(或者省略), 表示选项. 例如可以设置

StandardCopyOption.REPLACE_EXISTING
private static void copyByFiles(File srcFile,File desFile) throws IOException
{Files.copy(srcFile.toPath(), desFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}

注意 Files.copy 会保持文件的隐藏属性, 原来是隐藏的文件复制后也是隐藏的. 以上 7 种则不会.

6. 其他

(1)swing 布局

A. 网格布局

主 JFrame 采用了网格布局

setLayout(new GridLayout(3,1,5,3));

三行一列,因为只要三个按钮,选择源文件 (夹), 选择目标文件夹, 选择遍历方式.
选择遍历方式 / 复制方式的 JFrame 同样适用了网格布局:

showTraverseMethod.setLayout(new GridLayout(5,1,3,3));
showCopyMethod.setLayout(new GridLayout(4,2,5,5));

B. 居中

setBounds((int) (Toolkit.getDefaultToolkit().getScreenSize().getWidth() / 2) - 200,
(int) (Toolkit.getDefaultToolkit().getScreenSize().getHeight() / 2) - 200, 
400, 400);

高 400, 宽 400, 利用 ToolKit.getDefaultToolKit().getScreenSize()获取屏幕的高度和宽度实现居中.

C. 组件的添加与删除

由于在主 JFrame 中只有三个按钮, 选择完遍历方式后需要更新这个组件, 作者的做法是先删除这个组件在添加组件:

traverseMethodButton.setVisible(false);
remove(traverseMethodButton);
add(copyMethodButton);
copyMethodButton.setVisible(true);

设置它不可见再删除, 再添加另一组件, 再设置可见.

(2)进度条

进度条这个东西把作者搞得很惨啊 …… 其实就是新建一个线程就可以了.
核心代码为:

new Thread(() ->
    {
        int percent;
        while ((percent = getCopyPercent()) < 100)
        {
            try
            {Thread.sleep(100);
            }
            catch(InterruptedException e)
            {e.printStackTrace();
            }
            copyProgressBar.setValue(percent);
        }
    }
).start();

作者的 JProgressBar 是直接添加在一个 JFrame 中的, 不用什么太复杂的布局.
获取百分比后调用 setValue(), 一定要新建一个线程操作, 不然不能正常显示进度条.
另外复制的操作建议使用 SwingWorker.

SwingWorker<String,Object> copyTask = new SwingWorker<>()
{
    @Override
    protected String doInBackground()
    {
        try
        {if (traverseMethod[0])
                traverseByListFiles(src, des);
            else if (traverseMethod[1])
                traverseByList(src, des);
            else if (traverseMethod[2])
                traverseByGetFiles(src, des);
            else if (traverseMethod[3])
                traverseByCommonsIO(src, des);
            else if (traverseMethod[4])
                traverseByNIO2(src);
            else
            {showProgressBar.dispose();
                showMessage("遍历失败, 找不到遍历方法");
            }
        }
        catch (IOException e)
        {e.printStackTrace();
            showProgressBar.dispose();
            showMessage("未知错误复制失败");
        }
        finish(start);
        return null;
    }
};
copyTask.execute();

7. 测试

说了那么多来点实际的.
(以下所有的测试都是删除复制的文件后再进行新一次的复制.)

(1)1G 文件

1G file File.listFiles() File.list() plexus.util.FileUtils.getFiles() commons.io.FileUtils.listFiles Files.walkFileTree
FileIntput/OutputStream 20.189s 21.152s 18.249s 20.131s 21.782s
BufferedInput/OuputStream 17.761s 23.786s 22.118s 19.646s 16.806s
FileReader/Writer 61.334s 58.3s 58.904s 58.679s 55.762s
BufferedReader/Writer 63.287s 59.546s 56.664s 58.212s 59.884s
FileChannel 20.097s 22.272s 22.751s 22.765s 20.291s
FileChannel+ByteBuffer 18.857s 22.489s 23.148s 22.337s 17.213s
FileUtils.copyFile 25.398s 21.95s 22.808s 25.325s 22.483s
Files.copy 16.272s 14.166s 17.057s 14.987s 10.653s

文件的话其实纵向比较即可, 因为基本不用怎么遍历, 横向比较可以勉强看作求平均值.
对于非文本文件,FileReader/Writer 和 BufferedReader/Writer 没有太大的参考意义, 比如复制视频文件是打不开的, 而且复制出来的文件会变大. 对于单文件 Files.copy 的性能非常好,java 的 nio 果然厉害.

(2)10G 文件

10G file File.listFiles() File.list() plexus.util.FileUtils.getFiles() commons.io.FileUtils.listFiles Files.walkFileTree
FileIntput/OutputStream 171.427s 173.146s 172.611s 184.182s 250.251s
BufferedInput/OuputStream 203.509s 174.792s 167.727s 177.451s 217.53s
FileReader/Writer 187.55s 169.306s 226.571s 168.982s 218.303s
BufferedReader/Writer 155.134s 165.883s 166.192s 176.488s 206.306s
FileChannel 34.48s 35.445s 43.896s 41.827s 41.755s
FileChannel+ByteBuffer 175.632s 167.091s 178.455s 182.977s 183.763s
FileUtils.copyFile 203.997s 206.623s 201.01s 213.949s 208.739s
Files.copy 209.898s 186.889s 244.355s 222.336s 244.68s

这个 10G 的文件是文本文件.
现在可以看看 FileChannel 的这一行, 明显所花的时间要比其他要少, 为什么呢?
因为文件大于 2G.FileChannel 的 trasferTo 方法只能写入最多 2G 的文件, 所以对于大于 2G 的文件复制出来只有 2G, 因此 FileChannel 的这一行没有太大可比性. 对于文本文件,BufferedReader/Writer 的复制速度是最快的了, 其次是 FileInput/OutputStream. 对于单个大文件,apache 的 FileUtils 与 NIO 的 Files.copy 的速度比 FileInputStream 慢啊 …

(3)1G 目录

1G dir File.listFiles() File.list() plexus.util.FileUtils.getFiles() commons.io.FileUtils.listFiles Files.walkFileTree
FileIntput/OutputStream 23.549s 99.386s 143.388s 13.451s 10.773s
BufferedInput/OuputStream 6.306s 59.458s 20.704s 6.668s 6.616s
FileReader/Writer 49.059s 103.257s 51.995s 49.729s 51.509s
BufferedReader/Writer 59.932s 127.359s 51.731s 51.418s 50.317s
FileChannel 40.082s 71.713s 17.617s 15.782s 19.777s
FileChannel+ByteBuffer 33.355s 83.845s 19.68s 10.288s 17.152s
FileUtils.copyFile 24.163s 63.979s 8.277s 6.115s 19.513s
Files.copy 14.528s 28.215s 6.578s 5.883s 7.502s

对于目录的话可以考虑放弃 BufferedReader 与 FileReader 了, 除非全部是文本文件, 否则推荐使用 BufferedInput/OutputStream 与 Files.copy()进行复制, 工具类 FileUtils 的复制方法表现还是不错的, 但相比起 java 标准的 Files.copy 效率都差了.
对于 FileChannel 与配合缓冲使用的 FileChannel,1G 的话好像不相上下.
遍历方式的话 … 可以看到 plexus 的遍历方法表现差距很大, 而 apache 的 listFiles 或者 java nio 的 walkFileTree 比较稳定且速度还可以, 推荐使用这两种方式遍历目录.

(4)10G 目录

10G dir File.listFiles() File.list() plexus.util.FileUtils.getFiles() commons.io.FileUtils.listFiles Files.walkFileTree
FileIntput/OutputStream 216.822s 228.792s 227.908s 240.042s 191.863s
BufferedInput/OuputStream 218.599s 210.941s 207.375s 213.991s 167.614s
FileReader/Writer 536.747s 550.755s 550.415s 548.881s 516.684s
BufferedReader/Writer 587.612s 552.55s 549.716s 553.484s 498.18s
FileChannel 115.126s 117.538s 117.456s 118.207s 97.626s
FileChannel+ByteBuffer 225.887s 224.932s 222.077s 223.812s 180.177s
FileUtils.copyFile 233.724s 230.199s 232.133s 223.286s 189.737s
Files.copy 229.819s 227.562s 226.793s 226.78s 181.071s

FileReader 与 BufferedReader 这两行可以忽略了. 对于小文件用 FileChannel 的话还是不错的, 对于大文件一定要用 FileChannel 的话可以配合 ByteBuffer 使用, 不过从数据上看效果比 BufferedInput/OutputStream 要低.
再看看 org.apache.commons.io.FileUtils 与 java.nio.file.Files 的 copy, 差别不太, 效果接近, 但在 1G 的时候差距有点大.
遍历方式的话,java nio 的 walkFileTrees 最快.

当然这些测试仅供参考, 具体使用哪一个要看看具体环境, 另外这种方式把遍历与复制分开,apache 的 FileUtils 有方法可以直接复制目录的, 因此, 使用哪个更合适还需要个人具体测试.

8. 源码

作者比较偷懒全部仍在一个文件了. 七百行.

import java.awt.*;
import javax.swing.*;
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.util.*;
import org.apache.commons.io.*;

public class Test extends JFrame
{
    public static final long serialVersionUID = 12398129389122L;

    private JFrame showTraverseMethod = new JFrame("遍历方式");
    private JFrame showCopyMethod = new JFrame("复制方式");

    private JButton traverseMethodButton = new JButton("请选择遍历方式");
    private JButton copyMethodButton = new JButton("请选择复制方式");
    private JButton copyButton = new JButton("开始复制");

    private JButton traverseByListFiles = new JButton("File.listFiles()");
    private JButton traverseByList = new JButton("File.list()");
    private JButton traverseByGetFiles = new JButton("(plexus)getFiles()");
    private JButton traverseByCommonsIO = new JButton("Commons IO");
    private JButton traverseByNIO2 = new JButton("NIO2");

    private JButton copyByFileStream = new JButton("File stream");
    private JButton copyByBufferStream = new JButton("Buffer stream");
    private JButton copyByFileReader = new JButton("File reader");
    private JButton copyByBufferReader = new JButton("Buffer reader");
    private JButton copyByFileChannel = new JButton("File channel");
    private JButton copyByFileChannelWithBuffer = new JButton("File channel with buffer");
    private JButton copyByCommonsIO = new JButton("Commons IO");
    private JButton copyByFiles = new JButton("Files.copy");

    public Test()
    {JButton src = new JButton("选择源文件(夹)");
        src.addActionListener(
            event ->
            {JFileChooser fileChooser = new JFileChooser();
                fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
                fileChooser.showDialog(new Label(), "选择文件(夹)");
                FilesCopy.setSrc(fileChooser.getSelectedFile());
            }
        );
        JButton des = new JButton("选择目标文件夹");
        des.addActionListener(
            event ->
            {JFileChooser fileChooser = new JFileChooser();
                fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
                fileChooser.showDialog(new JLabel(),"选择文件夹");
                FilesCopy.setDes(fileChooser.getSelectedFile());
            }
        );

        traverseMethodButton.addActionListener(
            event ->
            {
                traverseByListFiles.addActionListener(
                    e->
                    {FilesCopy.setTraverseByListFiles();
                        showTraverseMethod.dispose();}
                );

                traverseByList.addActionListener(
                    e ->
                    {FilesCopy.setTraverseByList();
                        showTraverseMethod.dispose();}
                );

                traverseByGetFiles.addActionListener(
                    e ->
                    {FilesCopy.setTraverseByGetfiles();
                        showTraverseMethod.dispose();}
                );

                traverseByCommonsIO.addActionListener(
                    e ->
                    {FilesCopy.setTraverseByCommonsIO();
                        showTraverseMethod.dispose();}
                );

                traverseByNIO2.addActionListener(
                    e ->
                    {FilesCopy.setTraverseByNIO2();
                        showTraverseMethod.dispose();}
                );


                showTraverseMethod.setLayout(new GridLayout(5,1,3,3));
                showTraverseMethod.setTitle("遍历方式");
                showTraverseMethod.setBounds((int) (Toolkit.getDefaultToolkit().getScreenSize().getWidth() / 2) - 200,
                        (int) (Toolkit.getDefaultToolkit().getScreenSize().getHeight() / 2) - 200, 400, 400);
                showTraverseMethod.setVisible(true);
                showTraverseMethod.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                showTraverseMethod.add(traverseByListFiles);
                showTraverseMethod.add(traverseByList);
                showTraverseMethod.add(traverseByGetFiles);
                showTraverseMethod.add(traverseByCommonsIO);
                showTraverseMethod.add(traverseByNIO2);

                traverseMethodButton.setVisible(false);
                remove(traverseMethodButton);
                add(copyMethodButton);
                copyMethodButton.setVisible(true);
            }
        );

        copyMethodButton.addActionListener(
            event ->
            {
                copyByFileStream.addActionListener(
                    e ->
                    {FilesCopy.setCopyByFileStream();
                        showCopyMethod.dispose();}
                );

                copyByBufferStream.addActionListener(
                    e ->
                    {FilesCopy.setCopyByBufferStream();
                        showCopyMethod.dispose();}
                );

                copyByFileReader.addActionListener(
                    e ->
                    {FilesCopy.setCopyByFileReader();
                        showCopyMethod.dispose();}
                );

                copyByBufferReader.addActionListener(
                    e ->
                    {FilesCopy.setCopyByBufferReader();
                        showCopyMethod.dispose();}
                );

                copyByFileChannel.addActionListener(
                    e ->
                    {FilesCopy.setCopyByFileChannel();
                        showCopyMethod.dispose();}
                );

                copyByFileChannelWithBuffer.addActionListener(
                    e ->
                    {FilesCopy.setCopyByFileChannelWithBuffer();
                        showCopyMethod.dispose();}
                );

                copyByCommonsIO.addActionListener(
                    e ->
                    {FilesCopy.setCopyByCommonsIO();
                        showCopyMethod.dispose();}
                );

                copyByFiles.addActionListener(
                    e ->
                    {FilesCopy.setCopyByFiles();
                        showCopyMethod.dispose();}
                );

                showCopyMethod.setLayout(new GridLayout(4,2,5,5));
                showCopyMethod.setTitle("复制方式");
                showCopyMethod.setBounds((int) (Toolkit.getDefaultToolkit().getScreenSize().getWidth() / 2) - 200,
                        (int) (Toolkit.getDefaultToolkit().getScreenSize().getHeight() / 2) - 200, 400, 400);
                showCopyMethod.setVisible(true);
                showCopyMethod.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                showCopyMethod.add(copyByFileStream);
                showCopyMethod.add(copyByBufferStream);
                showCopyMethod.add(copyByFileReader);
                showCopyMethod.add(copyByBufferReader);
                showCopyMethod.add(copyByFileChannel);
                showCopyMethod.add(copyByFileChannelWithBuffer);
                showCopyMethod.add(copyByCommonsIO);
                showCopyMethod.add(copyByFiles);

                copyMethodButton.setVisible(false);
                remove(copyMethodButton);
                add(copyButton);
                copyButton.setVisible(true);
            }
        );

        copyButton.addActionListener(
            event ->
            {if(FilesCopy.haveSelectedSrcAndDes())
                {FilesCopy.copy();
                    copyButton.setVisible(false);
                    remove(copyButton);
                    add(traverseMethodButton);
                    traverseMethodButton.setVisible(true);
                }
                else
                    JOptionPane.showMessageDialog(null,"请先选择源文件 (夹) 与目标文件夹!");
            }
        );

        setLayout(new GridLayout(3,1,5,3));
        setTitle("复制文件");
        setBounds((int) (Toolkit.getDefaultToolkit().getScreenSize().getWidth() / 2) - 200,
                (int) (Toolkit.getDefaultToolkit().getScreenSize().getHeight() / 2) - 200, 400, 400);
        setVisible(true);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        add(src);
        add(des);
        add(traverseMethodButton);
    }

    public static void main(String[] args) {new Test();
    }
}

class FilesCopy
{
    private static File src = null;
    private static File des = null;
    private static long desSize = 0;
    private static long srcSize = 0;
    private static boolean [] traverseMethod = {false,false,false,false,false,false};
    private static boolean[] copyMethod = { false, false, false, false, false, false ,false,false};
    private static JFrame showProgressBar = new JFrame();
    private static JProgressBar copyProgressBar = new JProgressBar();
    private static JTextField textField = new JTextField();
    private static int index = 0;

    private static int getCopyPercent()
    {return (int)(desSize * 100.0 / srcSize);
    }

    private static void addCopySize() {desSize += 1024L;}

    public static void setTraverseByListFiles()
    {traverseMethod[0] = true;
    }

    private static void traverseByListFiles(File srcFile,File desFile) throws IOException
    {if(srcFile.isDirectory())
        {File[] files = srcFile.listFiles();
            assert files != null;
            for(File file : files)
            {File desFileOrDir = new File(desFile.getAbsolutePath() + File.separator + file.getName());
                if(file.isDirectory())
                {if(desFileOrDir.exists())
                        desFileOrDir.delete();
                    desFileOrDir.mkdirs();}
                traverseByListFiles(file, desFileOrDir);
            }
        }
        else {copyFile(srcFile, desFile);
        }
    }

    public static void setTraverseByList()
    {traverseMethod[1] = true;
    }

    private static void traverseByList(File srcFile,File desFile) throws IOException
    {if (srcFile.isDirectory())
        {String[] files = srcFile.list();
            assert files != null;
            for (String file : files)
            {File subSrcFile = new File(srcFile, file);
                File subDesFile = new File(desFile, file);
                if (subSrcFile.isDirectory())
                {if (subDesFile.exists())
                        subDesFile.delete();
                    subDesFile.mkdirs();}
                traverseByList(subSrcFile, subDesFile);
            }
        }
        else
        {copyFile(srcFile, desFile);
        }

    }

    public static void setTraverseByGetfiles()
    {traverseMethod[2] = true;
    }

    private static void traverseByGetFiles(File srcFile, File desFile) throws IOException
    {if (srcFile.isDirectory())
        {java.util.List<File> fileList = org.codehaus.plexus.util.FileUtils.getFiles(srcFile,null,null);
            for (File file : fileList)
            {File desFileOrDir = new File(desFile.getAbsolutePath() + File.separator + file.getName());
                if(file.isDirectory())
                {if(desFileOrDir.exists())
                        desFileOrDir.delete();
                    desFileOrDir.mkdirs();}
                traverseByListFiles(file, desFileOrDir);
            }
        }
        else
        {copyFile(srcFile, desFile);
        }
    }

    public static void setTraverseByCommonsIO()
    {traverseMethod[3] = true;
    }

    private static void traverseByCommonsIO(File srcFile, File desFile) throws IOException
    {if (srcFile.isDirectory())
        {Collection<File> files = org.apache.commons.io.FileUtils.listFiles(srcFile,null,false);
            for (File file : files)
            {File desFileOrDir = new File(desFile.getAbsolutePath() + File.separator + file.getName());
                if(file.isDirectory())
                {if(desFileOrDir.exists())
                        desFileOrDir.delete();
                    desFileOrDir.mkdirs();}
                traverseByCommonsIO(file, desFileOrDir);
            }
        }
        else {copyFile(srcFile, desFile);
        }
    }

    public static void setTraverseByNIO2()
    {traverseMethod[4] = true;
    }

    private static void traverseByNIO2(File srcFile) throws IOException
    {java.nio.file.Files.walkFileTree(srcFile.toPath(), new SimpleFileVisitor<>() {
            @Override
            public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {File d = new File(des.toString() + path.toAbsolutePath().toString().substring(src.toString().length()));
                new File(d.toString().substring(0, d.toString().lastIndexOf(File.separator))).mkdirs();
                copyFile(path.toFile(), d);
                return FileVisitResult.CONTINUE;
            }
        });
    }

    public static void setCopyByFileStream()
    {copyMethod[0] = true;
    }

    private static void copyByFileStream(File srcFile,File desFile) throws IOException
    {FileInputStream inputStream = new FileInputStream(srcFile);
        FileOutputStream outputStream = new FileOutputStream(desFile);
        byte [] b = new byte[1024];
        while(inputStream.read(b) != -1)
        {outputStream.write(b);
            addCopySize();}
        inputStream.close();
        outputStream.close();}

    public static void setCopyByBufferStream()
    {copyMethod[1] = true;
    }

    private static void copyByBufferStream(File srcFile,File desFile) throws IOException
    {BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(srcFile));
        BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(desFile));
        byte [] b = new byte[1024];
        while(inputStream.read(b) != -1)
        {addCopySize();
            outputStream.write(b);
        }
        inputStream.close();
        outputStream.close();}

    public static void setCopyByFileReader()
    {copyMethod[2] = true;
    }

    private static void copyByFileReader(File srcFile,File desFile) throws IOException
    {FileReader reader = new FileReader(srcFile);
        FileWriter writer = new FileWriter(desFile);

        char [] c = new char[1024];
        while(reader.read(c) != -1)
        {addCopySize();
            writer.write(c);
        }
        reader.close();
        writer.close();}

    public static void setCopyByBufferReader()
    {copyMethod[3] = true;
    }

    private static void copyByBufferReader(File srcFile,File desFile) throws IOException
    {BufferedReader reader = new BufferedReader(new FileReader(srcFile));
        BufferedWriter writer = new BufferedWriter(new FileWriter(desFile));

        char [] c = new char[1024];
        while(reader.read(c) != -1)
        {addCopySize();
            writer.write(c);
        }
        reader.close();
        writer.close();}

    public static void setCopyByFileChannel()
    {copyMethod[4] = true;
    }

    private static void copyByFileChannel(File srcFile,File desFile) throws IOException
    {FileChannel srcChannel = new FileInputStream(srcFile).getChannel();
        FileChannel desChannel = new FileOutputStream(desFile).getChannel();
        srcChannel.transferTo(0,srcChannel.size(),desChannel);
        srcChannel.close();
        desChannel.close();}

    public static void setCopyByFileChannelWithBuffer()
    {copyMethod[5] = true;
    }

    private static void copyByFileChannelWithBuffer(File srcFile,File desFile) throws IOException
    {FileChannel srcChannel = new FileInputStream(srcFile).getChannel();
        FileChannel desChannel = new FileOutputStream(desFile).getChannel();
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        while(srcChannel.read(buffer) != -1)
        {buffer.flip();
            desChannel.write(buffer);
            buffer.clear();
            addCopySize();}
        srcChannel.close();
        desChannel.close();}

    public static void setCopyByCommonsIO()
    {copyMethod[6] = true;
    }

    private static void copyByCommonsIO(File srcFile,File desFile) throws IOException
    {FileUtils.copyFile(srcFile, desFile);
    }

    public static void setCopyByFiles()
    {copyMethod[7] = true;
    }

    private static void copyByFiles(File srcFile,File desFile) throws IOException
    {Files.copy(srcFile.toPath(), desFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
    }

    public static void setSrc(File srcFile) {
        src = srcFile;
        if(srcFile.isDirectory())
            srcSize = org.apache.commons.io.FileUtils.sizeOfDirectory(srcFile);
        else
            srcSize = src.length();}

    public static void setDes(File desFile) {
        des = desFile;
        desSize = 0;
    }

    public static void setSrc(Path srcPath)
    {setSrc(srcPath.toFile());
    }

    public static void setDes(Path desPath)
    {setDes(desPath.toFile());
    }

    private static void copyFile(File srcFile,File desFile) throws IOException
    {if (copyMethod[0])
            copyByFileStream(srcFile,desFile);
        else if (copyMethod[1])
            copyByBufferStream(srcFile, desFile);
        else if (copyMethod[2])
            copyByFileReader(srcFile, desFile);
        else if (copyMethod[3])
            copyByBufferReader(srcFile, desFile);
        else if (copyMethod[4])
            copyByFileChannel(srcFile, desFile);
        else if (copyMethod[5])
            copyByFileChannelWithBuffer(srcFile, desFile);
        else if (copyMethod[6])
            copyByCommonsIO(srcFile, desFile);
        else if (copyMethod[7])
            copyByFiles(srcFile, desFile);
        else
            showMessage("复制失败, 找不到复制方法.");
    }

    private static void showMessage(String message)
    {JOptionPane.showMessageDialog(null, message);
    }

    public static boolean haveSelectedSrcAndDes()
    {return src != null && des != null;}

    public static void copy()
    {long start = System.currentTimeMillis();
        if(haveSelectedSrcAndDes())
        {if(src.isFile())
            {des = new File(des.getAbsolutePath()+File.separator+src.getName());
            }
            SwingWorker<String,Object> copyTask = new SwingWorker<>()
            {
                @Override
                protected String doInBackground()
                {
                    try
                    {if (traverseMethod[0])
                            traverseByListFiles(src, des);
                        else if (traverseMethod[1])
                            traverseByList(src, des);
                        else if (traverseMethod[2])
                            traverseByGetFiles(src, des);
                        else if (traverseMethod[3])
                            traverseByCommonsIO(src, des);
                        else if (traverseMethod[4])
                            traverseByNIO2(src);
                        else
                        {showProgressBar.dispose();
                            showMessage("遍历失败, 找不到遍历方法");
                        }
                    }
                    catch (IOException e)
                    {e.printStackTrace();
                        showProgressBar.dispose();
                        showMessage("未知错误复制失败");
                    }
                    finish(start);
                    return null;
                }
            };
            copyTask.execute();
            if (!copyMethod[4] && !copyMethod[6] && !copyMethod[7])
            {copyProgressBar.setMinimum(0);
                copyProgressBar.setMaximum(100);
                copyProgressBar.setValue(0);
                copyProgressBar.setVisible(true);
                copyProgressBar.setStringPainted(true);

                showProgressBar.add(copyProgressBar);
                showProgressBar.setTitle("复制进度");
                showProgressBar.setBounds((int) (Toolkit.getDefaultToolkit().getScreenSize().getWidth() / 2) - 150,
                        (int) (Toolkit.getDefaultToolkit().getScreenSize().getHeight() / 2) - 50, 300, 100);
                showProgressBar.setVisible(true);
                new Thread(() ->
                        {
                            int percent;
                            while ((percent = getCopyPercent()) < 100)
                            {
                                try
                                {Thread.sleep(100);
                                }
                                catch(InterruptedException e)
                                {e.printStackTrace();
                                }
                                copyProgressBar.setValue(percent);
                            }
                        }
                ).start();}
            else
            {final String [] text = {".","..","...","....",".....",".......","......",".....","....","...","..","."};
                textField.setVisible(true);
                textField.setHorizontalAlignment(JTextField.CENTER);
                textField.setEditable(false);
                showProgressBar.add(textField);
                showProgressBar.setTitle("复制中");
                showProgressBar.setBounds((int) (Toolkit.getDefaultToolkit().getScreenSize().getWidth() / 2) - 120,
                        (int) (Toolkit.getDefaultToolkit().getScreenSize().getHeight() / 2) - 40, 240, 80);
                showProgressBar.setVisible(true);

                new Thread(() ->
                        {while (getCopyPercent() < 100)
                            {
                                try
                                {Thread.sleep(400);
                                }
                                catch(InterruptedException e)
                                {e.printStackTrace();
                                }
                                if(index < text.length)
                                    textField.setText("复制中"+text[index++]);
                                else
                                    index = 0;
                            }
                        }
                ).start();}
        }
    }

    private static void finish(long start)
    {long end = System.currentTimeMillis();
        showProgressBar.dispose();
        showMessage("复制成功, 用时:" + (end - start) / 1000.0 + "s");

        copyProgressBar.setVisible(false);
        showProgressBar.remove(copyProgressBar);
        textField.setVisible(false);
        showProgressBar.remove(textField);

        Arrays.fill(traverseMethod, false);
        Arrays.fill(copyMethod, false);
        des = src = null;
        desSize = srcSize;
    }
}
退出移动版