Java有几种文件拷贝方式?哪一种最高效?

时间:2024-05-22 19:42:47

典型回答

Java有多种比较典型的文件拷贝实现方法。

方法1:利用java.io类库。直接为源文件创建一个FileInputStream负责读取,然后再为目标文件创建一个FileOutputStream负责写入:

public static void copyFileByStream(File source, File target) throws IOException {
  try (InputStream is = new FileInputStream(source);
    OutputStream os = new FileOutputStream(target);) {
    byte[] buffer = new byte[1024];
    int length;
    while ((length = is.read(buffer)) > 0) {
      os.write(buffer, 0, length);
    }
  }
}

方法2:利用java.nio类库提供的transferTo或transferFrom方法实现:

public static void copyFileByChannel(File source, File target) throws IOException {
  try (FileChannel sc = new FileInputStream(source).getChannel();
    FileChannel tc = new FileOutputStream(target).getChannel();) {
    long count = sc.size();
    while (count > 0) {
      long transferred = sc.transferTo(sc.position(), count, tc);
      count -= transferred;
    }
  }
}

方法3:直接调用java.nio.file.Files.copy()的实现。

关于拷贝效率的问题,其实与操作系统和配置等有关,总体上来说,NIO transferTo/transferFrom的方式可能更快,因为它更能利用现代操作系统底层机制,避免不必要拷贝和上下文切换。

知识扩展

1、拷贝实现机制分析

上面方法1与方法2在本质上有明显的区别。

首先,你需要理解用户态空间(User Space)和内核态空间(Kernel Space),这是操作系统层面的基本概念。操作系统内核、硬件驱动等运行在内核态空间,具有相对高的特权;而用户态空间,则是给普通应用和服务使用。

在方法1中,我们使用输入输出流进行读写时,实际上是进行了多次上下文切换。比如应用读取数据时,先在内核态将数据从磁盘读取到内存缓存,再切换到用户态将数据从内核缓存读取到用户缓存。写入操作也是类似的,仅仅是步骤相反。参考下图:

Java有几种文件拷贝方式?哪一种最高效?

所以,这种方式会带来一定的额外开销,可能会降低IO效率。

在方法2中,采用NIO transferTo实现,在Linux和Unix上,则会使用到零拷贝技术。数据传输并不需要用户态参与,省去了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。注意,transferTo不仅仅是可以用在文件拷贝中,与其类似的,例如读取磁盘文件,然后进行Socket发送,同样可以享受这种机制带来的性能和扩展性能提高。transferTo传输过程如下图:

Java有几种文件拷贝方式?哪一种最高效?

2、Files.copy方法解析

Java 7开始提供的java.nio.file.Files中提供了一系列copy方法。从Files工具类所在的包看,直觉告诉我们,其内部应该使用的是NIO transferTo。但事实真是如此么?

实际上Files中提供了三种不同的copy方法:

public static long copy(InputStream in, Path target, CopyOption... options) throws IOException
public static long copy(Path source, OutputStream out) throws IOException
public static Path copy(Path source, Path target, CopyOption... options) throws IOException

了解这个问题的真相最好的方法就是查看源代码。这里我们跳过源代码分析过程,直接说结论。很不幸,这三个方法都是用户态拷贝。

3、实践角度总结

之前谈了不少机制和源码,这里简单从实践角度总结一下,如何提高类似拷贝等IO操作的性能,有一些宽泛的原则:

  • 在程序中,使用缓存等机制,合理减少IO次数。
  • 使用transferTo等机制,减少上下文切换和额外IO操作。
  • 尽量减少不必要的转换过程,比如编/解码;对象序列化和反序列化。比如操作文本文件或者网络通信,如果不是过程中需要使用文本信息,可以考虑不要将二进制信息转换成字符串,直接传输二进制信息。