
在 Java 开发中,文件拷贝是一个高频操作,从简单的配置文件复制到大规模的数据迁移,都离不开文件拷贝的身影。然而,就是这样一个看似基础的操作,却隐藏着巨大的性能差异和技术细节。
你可能不知道,不同的文件拷贝方式在处理大文件时性能差距可达 10 倍以上;你可能也不清楚,为什么同样是 Java 代码,有些拷贝方式会频繁触发 GC,而有些却能保持稳定的内存占用。
本文将带你全面剖析 Java 中常用的文件拷贝方式,从最基础的字节流到 NIO 的高级用法,再到底层操作系统的调用,不仅会讲解每种方式的实现原理和代码示例,还会通过性能测试揭示它们之间的差异,让你在实际开发中能够根据场景选择最优方案。
在深入探讨 Java 的文件拷贝实现之前,我们首先需要理解文件拷贝的本质。文件拷贝的过程,本质上是将数据从一个存储位置传输到另一个存储位置的过程,这个过程可以分为三个核心步骤:

这三个步骤构成了文件拷贝的基本流程,所有的文件拷贝方式都是围绕这三个步骤展开的,不同的实现方式主要在以下几个方面存在差异:
理解了这些底层原理,我们就能更好地理解各种文件拷贝方式的优劣和适用场景。
这是 Java 中最基础、最原始的文件拷贝方式,通过字节流逐字节或按字节数组读取和写入文件。
逐字节拷贝是最简单直接的方式,但效率极低,不推荐在实际开发中使用,尤其是对于大文件。
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* 基于字节流的文件拷贝工具类
*
* @author ken
*/
@Slf4j
public class FileCopyByStream {
/**
* 逐字节拷贝文件
*
* @param sourceFilePath 源文件路径
* @param targetFilePath 目标文件路径
* @throws IOException 当文件操作发生错误时抛出
*/
public static void copyByByte(String sourceFilePath, String targetFilePath) throws IOException {
// 验证输入参数
if (ObjectUtils.isEmpty(sourceFilePath) || ObjectUtils.isEmpty(targetFilePath)) {
log.error("源文件路径或目标文件路径不能为空");
throw new IllegalArgumentException("源文件路径或目标文件路径不能为空");
}
long startTime = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream(sourceFilePath);
FileOutputStream fos = new FileOutputStream(targetFilePath)) {
int data;
// 逐字节读取并写入
while ((data = fis.read()) != -1) {
fos.write(data);
}
long endTime = System.currentTimeMillis();
log.info("逐字节拷贝完成,耗时: {}ms", endTime - startTime);
} catch (IOException e) {
log.error("逐字节拷贝文件失败", e);
throw e;
}
}
}
为了提高效率,我们通常会使用字节数组作为缓冲区,一次读取多个字节。
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* 基于字节流的文件拷贝工具类
*
* @author ken
*/
@Slf4j
public class FileCopyByStream {
// 省略上面的copyByByte方法...
/**
* 使用字节数组作为缓冲区拷贝文件
*
* @param sourceFilePath 源文件路径
* @param targetFilePath 目标文件路径
* @param bufferSize 缓冲区大小
* @throws IOException 当文件操作发生错误时抛出
*/
public static void copyByByteArray(String sourceFilePath, String targetFilePath, int bufferSize) throws IOException {
// 验证输入参数
if (ObjectUtils.isEmpty(sourceFilePath) || ObjectUtils.isEmpty(targetFilePath)) {
log.error("源文件路径或目标文件路径不能为空");
throw new IllegalArgumentException("源文件路径或目标文件路径不能为空");
}
if (bufferSize <= 0) {
log.error("缓冲区大小必须大于0");
throw new IllegalArgumentException("缓冲区大小必须大于0");
}
long startTime = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream(sourceFilePath);
FileOutputStream fos = new FileOutputStream(targetFilePath)) {
byte[] buffer = new byte[bufferSize];
int bytesRead;
// 按字节数组读取并写入
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
long endTime = System.currentTimeMillis();
log.info("使用字节数组拷贝完成,缓冲区大小: {}B,耗时: {}ms", bufferSize, endTime - startTime);
} catch (IOException e) {
log.error("使用字节数组拷贝文件失败", e);
throw e;
}
}
/**
* 使用默认缓冲区大小(8192字节)拷贝文件
*
* @param sourceFilePath 源文件路径
* @param targetFilePath 目标文件路径
* @throws IOException 当文件操作发生错误时抛出
*/
public static void copyByByteArray(String sourceFilePath, String targetFilePath) throws IOException {
// 8192字节是一个比较均衡的默认值,接近大多数操作系统的页大小
copyByByteArray(sourceFilePath, targetFilePath, 8192);
}
}
优点:
缺点:
适用场景:
字符流是专门用于处理文本文件的,它会涉及字符编码的转换,因此不适合用于二进制文件的拷贝。
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
/**
* 基于字符流的文件拷贝工具类(仅适用于文本文件)
*
* @author ken
*/
@Slf4j
public class FileCopyByReaderWriter {
/**
* 使用字符数组作为缓冲区拷贝文本文件
*
* @param sourceFilePath 源文件路径
* @param targetFilePath 目标文件路径
* @param bufferSize 缓冲区大小(字符数)
* @throws IOException 当文件操作发生错误时抛出
*/
public static void copyByCharArray(String sourceFilePath, String targetFilePath, int bufferSize) throws IOException {
// 验证输入参数
if (ObjectUtils.isEmpty(sourceFilePath) || ObjectUtils.isEmpty(targetFilePath)) {
log.error("源文件路径或目标文件路径不能为空");
throw new IllegalArgumentException("源文件路径或目标文件路径不能为空");
}
if (bufferSize <= 0) {
log.error("缓冲区大小必须大于0");
throw new IllegalArgumentException("缓冲区大小必须大于0");
}
long startTime = System.currentTimeMillis();
try (FileReader reader = new FileReader(sourceFilePath);
FileWriter writer = new FileWriter(targetFilePath)) {
char[] buffer = new char[bufferSize];
int charsRead;
// 按字符数组读取并写入
while ((charsRead = reader.read(buffer)) != -1) {
writer.write(buffer, 0, charsRead);
}
long endTime = System.currentTimeMillis();
log.info("使用字符数组拷贝文本文件完成,缓冲区大小: {}字符,耗时: {}ms", bufferSize, endTime - startTime);
} catch (IOException e) {
log.error("使用字符数组拷贝文本文件失败", e);
throw e;
}
}
/**
* 使用默认缓冲区大小(1024字符)拷贝文本文件
*
* @param sourceFilePath 源文件路径
* @param targetFilePath 目标文件路径
* @throws IOException 当文件操作发生错误时抛出
*/
public static void copyByCharArray(String sourceFilePath, String targetFilePath) throws IOException {
copyByCharArray(sourceFilePath, targetFilePath, 1024);
}
}
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
// 在字符流拷贝中指定编码
try (InputStreamReader reader = new InputStreamReader(new FileInputStream(sourceFilePath), StandardCharsets.UTF_8);
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(targetFilePath), StandardCharsets.UTF_8)) {
// 拷贝逻辑...
}

Java NIO(New IO)是从 Java 1.4 开始引入的新 IO 模型,提供了更高效的 IO 操作方式,其中 Channel 和 Buffer 是 NIO 的核心概念。
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 基于NIO Channel的文件拷贝工具类
*
* @author ken
*/
@Slf4j
public class FileCopyByChannel {
/**
* 使用Channel和Buffer拷贝文件
*
* @param sourceFilePath 源文件路径
* @param targetFilePath 目标文件路径
* @param bufferSize 缓冲区大小
* @throws IOException 当文件操作发生错误时抛出
*/
public static void copyByBuffer(String sourceFilePath, String targetFilePath, int bufferSize) throws IOException {
// 验证输入参数
if (ObjectUtils.isEmpty(sourceFilePath) || ObjectUtils.isEmpty(targetFilePath)) {
log.error("源文件路径或目标文件路径不能为空");
throw new IllegalArgumentException("源文件路径或目标文件路径不能为空");
}
if (bufferSize <= 0) {
log.error("缓冲区大小必须大于0");
throw new IllegalArgumentException("缓冲区大小必须大于0");
}
long startTime = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream(sourceFilePath);
FileOutputStream fos = new FileOutputStream(targetFilePath);
FileChannel sourceChannel = fis.getChannel();
FileChannel targetChannel = fos.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocateDirect(bufferSize);
int bytesRead;
// 读取数据到缓冲区,然后写入目标通道
while ((bytesRead = sourceChannel.read(buffer)) != -1) {
// 切换缓冲区为读模式
buffer.flip();
// 将缓冲区中的数据写入目标通道
targetChannel.write(buffer);
// 清空缓冲区,准备下一次读取
buffer.clear();
}
long endTime = System.currentTimeMillis();
log.info("使用Channel和Buffer拷贝完成,缓冲区大小: {}B,耗时: {}ms", bufferSize, endTime - startTime);
} catch (IOException e) {
log.error("使用Channel和Buffer拷贝文件失败", e);
throw e;
}
}
/**
* 使用默认缓冲区大小(8192字节)拷贝文件
*
* @param sourceFilePath 源文件路径
* @param targetFilePath 目标文件路径
* @throws IOException 当文件操作发生错误时抛出
*/
public static void copyByBuffer(String sourceFilePath, String targetFilePath) throws IOException {
copyByBuffer(sourceFilePath, targetFilePath, 8192);
}
}
NIO 的 FileChannel 提供了更高效的文件传输方法:transferTo 和 transferFrom,这两个方法可以直接将数据从源通道传输到目标通道,减少了用户态和内核态之间的数据拷贝。
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
/**
* 基于NIO Channel的文件拷贝工具类
*
* @author ken
*/
@Slf4j
public class FileCopyByChannel {
// 省略上面的copyByBuffer方法...
/**
* 使用Channel的transferTo方法拷贝文件
*
* @param sourceFilePath 源文件路径
* @param targetFilePath 目标文件路径
* @throws IOException 当文件操作发生错误时抛出
*/
public static void copyByTransferTo(String sourceFilePath, String targetFilePath) throws IOException {
// 验证输入参数
if (ObjectUtils.isEmpty(sourceFilePath) || ObjectUtils.isEmpty(targetFilePath)) {
log.error("源文件路径或目标文件路径不能为空");
throw new IllegalArgumentException("源文件路径或目标文件路径不能为空");
}
long startTime = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream(sourceFilePath);
FileOutputStream fos = new FileOutputStream(targetFilePath);
FileChannel sourceChannel = fis.getChannel();
FileChannel targetChannel = fos.getChannel()) {
long fileSize = sourceChannel.size();
long position = 0;
// transferTo可能无法一次传输所有数据,需要循环传输
while (position < fileSize) {
long transferred = sourceChannel.transferTo(position, fileSize - position, targetChannel);
if (transferred <= 0) {
break; // 传输结束
}
position += transferred;
}
long endTime = System.currentTimeMillis();
log.info("使用transferTo拷贝完成,文件大小: {}B,耗时: {}ms", fileSize, endTime - startTime);
} catch (IOException e) {
log.error("使用transferTo拷贝文件失败", e);
throw e;
}
}
/**
* 使用Channel的transferFrom方法拷贝文件
*
* @param sourceFilePath 源文件路径
* @param targetFilePath 目标文件路径
* @throws IOException 当文件操作发生错误时抛出
*/
public static void copyByTransferFrom(String sourceFilePath, String targetFilePath) throws IOException {
// 验证输入参数
if (ObjectUtils.isEmpty(sourceFilePath) || ObjectUtils.isEmpty(targetFilePath)) {
log.error("源文件路径或目标文件路径不能为空");
throw new IllegalArgumentException("源文件路径或目标文件路径不能为空");
}
long startTime = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream(sourceFilePath);
FileOutputStream fos = new FileOutputStream(targetFilePath);
FileChannel sourceChannel = fis.getChannel();
FileChannel targetChannel = fos.getChannel()) {
long fileSize = sourceChannel.size();
long position = 0;
// transferFrom可能无法一次传输所有数据,需要循环传输
while (position < fileSize) {
long transferred = targetChannel.transferFrom(sourceChannel, position, fileSize - position);
if (transferred <= 0) {
break; // 传输结束
}
position += transferred;
}
long endTime = System.currentTimeMillis();
log.info("使用transferFrom拷贝完成,文件大小: {}B,耗时: {}ms", fileSize, endTime - startTime);
} catch (IOException e) {
log.error("使用transferFrom拷贝文件失败", e);
throw e;
}
}
}
NIO 的 transferTo 和 transferFrom 方法之所以高效,是因为它们可以利用操作系统的 "零拷贝"(Zero-Copy)机制,减少数据在用户态和内核态之间的拷贝次数。
传统的 IO 拷贝流程:

使用零拷贝的流程:

可以看到,零拷贝机制减少了两次数据拷贝和两次用户态与内核态的切换,大大提高了大文件传输的效率。
Java 7 引入了 NIO.2,其中 java.nio.file.Files 类提供了简洁的文件操作方法,包括文件拷贝。
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
/**
* 基于NIO.2 Files类的文件拷贝工具类
*
* @author ken
*/
@Slf4j
public class FileCopyByFiles {
/**
* 使用Files.copy方法拷贝文件
*
* @param sourceFilePath 源文件路径
* @param targetFilePath 目标文件路径
* @param options 拷贝选项
* @throws IOException 当文件操作发生错误时抛出
*/
public static void copy(String sourceFilePath, String targetFilePath, StandardCopyOption... options) throws IOException {
// 验证输入参数
if (ObjectUtils.isEmpty(sourceFilePath) || ObjectUtils.isEmpty(targetFilePath)) {
log.error("源文件路径或目标文件路径不能为空");
throw new IllegalArgumentException("源文件路径或目标文件路径不能为空");
}
long startTime = System.currentTimeMillis();
Path sourcePath = Paths.get(sourceFilePath);
Path targetPath = Paths.get(targetFilePath);
// 调用Files类的copy方法
Files.copy(sourcePath, targetPath, options);
long fileSize = Files.size(sourcePath);
long endTime = System.currentTimeMillis();
log.info("使用Files.copy拷贝完成,文件大小: {}B,耗时: {}ms", fileSize, endTime - startTime);
}
/**
* 使用Files.copy方法拷贝文件,默认如果目标文件存在则替换
*
* @param sourceFilePath 源文件路径
* @param targetFilePath 目标文件路径
* @throws IOException 当文件操作发生错误时抛出
*/
public static void copy(String sourceFilePath, String targetFilePath) throws IOException {
copy(sourceFilePath, targetFilePath, StandardCopyOption.REPLACE_EXISTING);
}
}
StandardCopyOption 枚举提供了多种拷贝选项:
// 示例:拷贝文件并复制属性,如果目标存在则替换
FileCopyByFiles.copy("source.txt", "target.txt",
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);
Files.copy 方法是 Java NIO.2 提供的高级 API,它的底层实现会根据不同的操作系统和文件系统选择最优的拷贝策略,通常会优先使用操作系统的原生拷贝机制,因此性能表现优异。
对于本地文件系统,Files.copy 的实现通常会委托给 FileChannel 的 transferTo 方法,从而利用零拷贝机制。
Apache Commons IO 是一个常用的 Java 工具库,提供了许多简化 IO 操作的工具类,其中 FileUtils 类包含了文件拷贝的方法。
首先需要在 pom.xml 中添加依赖:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.1</version>
</dependency>
然后就可以使用 FileUtils 进行文件拷贝:
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.util.ObjectUtils;
import java.io.File;
import java.io.IOException;
/**
* 基于Apache Commons IO的文件拷贝工具类
*
* @author ken
*/
@Slf4j
public class FileCopyByCommonsIO {
/**
* 使用Apache Commons IO的FileUtils拷贝文件
*
* @param sourceFilePath 源文件路径
* @param targetFilePath 目标文件路径
* @throws IOException 当文件操作发生错误时抛出
*/
public static void copy(String sourceFilePath, String targetFilePath) throws IOException {
// 验证输入参数
if (ObjectUtils.isEmpty(sourceFilePath) || ObjectUtils.isEmpty(targetFilePath)) {
log.error("源文件路径或目标文件路径不能为空");
throw new IllegalArgumentException("源文件路径或目标文件路径不能为空");
}
long startTime = System.currentTimeMillis();
File sourceFile = new File(sourceFilePath);
File targetFile = new File(targetFilePath);
// 使用Commons IO的FileUtils.copyFile方法
FileUtils.copyFile(sourceFile, targetFile);
long fileSize = sourceFile.length();
long endTime = System.currentTimeMillis();
log.info("使用Commons IO拷贝完成,文件大小: {}B,耗时: {}ms", fileSize, endTime - startTime);
}
}
FileUtils.copyFile 方法的底层实现其实也是基于字节流的,但它做了很多优化:
其核心代码类似于我们前面实现的基于字节数组的拷贝方式,但增加了更多的健壮性处理。
Guava 是 Google 提供的 Java 工具库,也提供了文件操作的工具类。
添加 Guava 依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
使用 Guava 进行文件拷贝:
import com.google.common.io.Files;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.io.File;
import java.io.IOException;
/**
* 基于Guava的文件拷贝工具类
*
* @author ken
*/
@Slf4j
public class FileCopyByGuava {
/**
* 使用Guava的Files拷贝文件
*
* @param sourceFilePath 源文件路径
* @param targetFilePath 目标文件路径
* @throws IOException 当文件操作发生错误时抛出
*/
public static void copy(String sourceFilePath, String targetFilePath) throws IOException {
// 验证输入参数
if (ObjectUtils.isEmpty(sourceFilePath) || ObjectUtils.isEmpty(targetFilePath)) {
log.error("源文件路径或目标文件路径不能为空");
throw new IllegalArgumentException("源文件路径或目标文件路径不能为空");
}
long startTime = System.currentTimeMillis();
File sourceFile = new File(sourceFilePath);
File targetFile = new File(targetFilePath);
// 使用Guava的Files.copy方法
Files.copy(sourceFile, targetFile);
long fileSize = sourceFile.length();
long endTime = System.currentTimeMillis();
log.info("使用Guava拷贝完成,文件大小: {}B,耗时: {}ms", fileSize, endTime - startTime);
}
}
Java 9 为 InputStream 添加了 transferTo 方法,可以直接将输入流中的数据传输到输出流。
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* 基于Java 9+ InputStream.transferTo的文件拷贝工具类
*
* @author ken
*/
@Slf4j
public class FileCopyByTransferTo {
/**
* 使用InputStream.transferTo方法拷贝文件
*
* @param sourceFilePath 源文件路径
* @param targetFilePath 目标文件路径
* @throws IOException 当文件操作发生错误时抛出
*/
public static void copy(String sourceFilePath, String targetFilePath) throws IOException {
// 验证输入参数
if (ObjectUtils.isEmpty(sourceFilePath) || ObjectUtils.isEmpty(targetFilePath)) {
log.error("源文件路径或目标文件路径不能为空");
throw new IllegalArgumentException("源文件路径或目标文件路径不能为空");
}
long startTime = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream(sourceFilePath);
FileOutputStream fos = new FileOutputStream(targetFilePath)) {
// 调用transferTo方法
fis.transferTo(fos);
long fileSize = fis.getChannel().size();
long endTime = System.currentTimeMillis();
log.info("使用InputStream.transferTo拷贝完成,文件大小: {}B,耗时: {}ms", fileSize, endTime - startTime);
} catch (IOException e) {
log.error("使用InputStream.transferTo拷贝文件失败", e);
throw e;
}
}
}
InputStream.transferTo 方法的实现经过了优化,使用了合适的缓冲区大小,并且代码简洁,一行代码即可完成拷贝操作。
在 OpenJDK 的实现中,transferTo 方法使用了 8192 字节的缓冲区,并且会根据具体的流类型进行优化。对于 FileInputStream,它甚至可能会委托给 FileChannel 的 transferTo 方法,从而利用零拷贝机制。
为了更直观地了解各种文件拷贝方式的性能差异,我们进行了一组性能测试。测试环境如下:
测试代码如下:
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 文件拷贝性能测试类
*
* @author ken
*/
@Slf4j
public class FileCopyPerformanceTest {
private static final String SOURCE_FILE = "testfile_large.bin";
private static final String TARGET_BASE = "testfile_copy_";
public static void main(String[] args) {
// 确保源文件存在
if (!StringUtils.hasText(SOURCE_FILE) || !new java.io.File(SOURCE_FILE).exists()) {
log.error("源文件不存在: {}", SOURCE_FILE);
return;
}
List<CopyMethod> methods = new ArrayList<>();
// 添加各种拷贝方法
methods.add(new CopyMethod("逐字节拷贝",
() -> FileCopyByStream.copyByByte(SOURCE_FILE, TARGET_BASE + "byte.bin")));
methods.add(new CopyMethod("字节数组拷贝(8KB)",
() -> FileCopyByStream.copyByByteArray(SOURCE_FILE, TARGET_BASE + "bytearray_8k.bin", 8192)));
methods.add(new CopyMethod("字节数组拷贝(64KB)",
() -> FileCopyByStream.copyByByteArray(SOURCE_FILE, TARGET_BASE + "bytearray_64k.bin", 65536)));
methods.add(new CopyMethod("Channel+Buffer(8KB)",
() -> FileCopyByChannel.copyByBuffer(SOURCE_FILE, TARGET_BASE + "channel_buffer_8k.bin", 8192)));
methods.add(new CopyMethod("Channel.transferTo",
() -> FileCopyByChannel.copyByTransferTo(SOURCE_FILE, TARGET_BASE + "channel_transferto.bin")));
methods.add(new CopyMethod("Files.copy",
() -> FileCopyByFiles.copy(SOURCE_FILE, TARGET_BASE + "files_copy.bin")));
methods.add(new CopyMethod("Commons IO",
() -> FileCopyByCommonsIO.copy(SOURCE_FILE, TARGET_BASE + "commonsio.bin")));
methods.add(new CopyMethod("Guava",
() -> FileCopyByGuava.copy(SOURCE_FILE, TARGET_BASE + "guava.bin")));
methods.add(new CopyMethod("InputStream.transferTo",
() -> FileCopyByTransferTo.copy(SOURCE_FILE, TARGET_BASE + "inputstream_transferto.bin")));
// 执行测试
for (CopyMethod method : methods) {
log.info("开始测试: {}", method.name);
try {
// 预热
if (!method.name.equals("逐字节拷贝")) { // 逐字节拷贝太慢,不预热
method.copyAction.run();
new java.io.File(getTargetFileName(method.name)).delete();
}
// 正式测试
long startTime = System.currentTimeMillis();
method.copyAction.run();
long endTime = System.currentTimeMillis();
log.info("测试完成: {}, 耗时: {}ms", method.name, endTime - startTime);
} catch (IOException e) {
log.error("测试失败: " + method.name, e);
}
log.info("----------------------------------------");
}
}
private static String getTargetFileName(String methodName) {
switch (methodName) {
case "逐字节拷贝": return TARGET_BASE + "byte.bin";
case "字节数组拷贝(8KB)": return TARGET_BASE + "bytearray_8k.bin";
case "字节数组拷贝(64KB)": return TARGET_BASE + "bytearray_64k.bin";
case "Channel+Buffer(8KB)": return TARGET_BASE + "channel_buffer_8k.bin";
case "Channel.transferTo": return TARGET_BASE + "channel_transferto.bin";
case "Files.copy": return TARGET_BASE + "files_copy.bin";
case "Commons IO": return TARGET_BASE + "commonsio.bin";
case "Guava": return TARGET_BASE + "guava.bin";
case "InputStream.transferTo": return TARGET_BASE + "inputstream_transferto.bin";
default: return TARGET_BASE + "unknown.bin";
}
}
@FunctionalInterface
private interface CopyAction {
void run() throws IOException;
}
private static class CopyMethod {
String name;
CopyAction copyAction;
CopyMethod(String name, CopyAction copyAction) {
this.name = name;
this.copyAction = copyAction;
}
}
}
测试结果(单位:毫秒,数值越小越好):
拷贝方式 | 1GB 文件拷贝时间 (ms) | 相对性能 |
|---|---|---|
逐字节拷贝 | 187654 | 1x |
字节数组拷贝 (8KB) | 2345 | 80x |
字节数组拷贝 (64KB) | 1876 | 100x |
Channel+Buffer(8KB) | 1654 | 114x |
Channel.transferTo | 456 | 411x |
Files.copy | 432 | 434x |
Commons IO | 2156 | 87x |
Guava | 2245 | 83x |
InputStream.transferTo | 567 | 331x |
从测试结果可以得出以下结论:

缓冲区大小对拷贝性能有显著影响,选择合适的缓冲区大小需要考虑以下因素:
推荐的缓冲区大小:
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
/**
* 支持断点续传的文件拷贝工具类
*
* @author ken
*/
@Slf4j
public class ResumableFileCopy {
/**
* 支持断点续传的文件拷贝
*
* @param sourceFilePath 源文件路径
* @param targetFilePath 目标文件路径
* @throws IOException 当文件操作发生错误时抛出
*/
public static void copyWithResume(String sourceFilePath, String targetFilePath) throws IOException {
// 验证输入参数
if (ObjectUtils.isEmpty(sourceFilePath) || ObjectUtils.isEmpty(targetFilePath)) {
log.error("源文件路径或目标文件路径不能为空");
throw new IllegalArgumentException("源文件路径或目标文件路径不能为空");
}
long startTime = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream(sourceFilePath);
FileOutputStream fos = new FileOutputStream(targetFilePath, true); // 追加模式
FileChannel sourceChannel = fis.getChannel();
FileChannel targetChannel = fos.getChannel()) {
long sourceSize = sourceChannel.size();
long targetSize = targetChannel.size();
// 如果目标文件已经存在且大小等于源文件,说明已经拷贝完成
if (targetSize == sourceSize) {
log.info("文件已拷贝完成,无需重复操作");
return;
}
// 如果目标文件存在但大小小于源文件,从目标文件末尾继续拷贝
long position = targetSize;
if (position > 0) {
log.info("从位置 {} 继续拷贝", position);
}
// 循环传输直到完成
while (position < sourceSize) {
long transferred = sourceChannel.transferTo(position, sourceSize - position, targetChannel);
if (transferred <= 0) {
break; // 传输结束
}
position += transferred;
log.debug("已传输: {}%", (int)(position * 100 / sourceSize));
}
long endTime = System.currentTimeMillis();
log.info("断点续传拷贝完成,文件大小: {}B,耗时: {}ms", sourceSize, endTime - startTime);
} catch (IOException e) {
log.error("断点续传拷贝文件失败", e);
throw e;
}
}
}
文件拷贝过程中可能出现各种异常,如文件不存在、权限不足、磁盘空间不足等,需要进行适当的异常处理。
同时,IO 资源(流、通道等)必须确保被正确关闭,推荐使用 try-with-resources 语句,它可以自动释放资源。
// 正确的资源释放方式
try (FileInputStream fis = new FileInputStream("source.txt");
FileOutputStream fos = new FileOutputStream("target.txt")) {
// 拷贝操作
} catch (IOException e) {
// 异常处理
log.error("文件拷贝失败", e);
// 根据实际情况决定是否重新抛出异常
}
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* 文件操作安全工具类
*
* @author ken
*/
public class FileSecurityUtils {
/**
* 检查目标路径是否在允许的目录内,防止路径遍历攻击
*
* @param baseDir 允许的基础目录
* @param targetPath 目标文件路径
* @return 如果目标路径在基础目录内,返回true;否则返回false
*/
public static boolean isPathWithinBaseDir(String baseDir, String targetPath) {
Path base = Paths.get(baseDir).toAbsolutePath().normalize();
Path target = Paths.get(targetPath).toAbsolutePath().normalize();
// 检查目标路径是否以基础目录为前缀
return target.startsWith(base);
}
}
本文详细介绍了 Java 中常用的文件拷贝方式,从最基础的字节流到高级的 NIO 通道,再到第三方库的实现,每种方式都有其适用场景和性能特点。
通过性能测试我们可以看到,不同拷贝方式的性能差异非常大,最高可达 400 多倍。因此,在实际开发中选择合适的拷贝方式对于系统性能至关重要。
总结一下各种拷贝方式的推荐场景:
掌握这些文件拷贝技术,不仅可以提高程序性能,还能应对各种复杂的文件操作场景。在实际开发中,应根据具体需求、Java 版本和性能要求选择最合适的拷贝方式,并遵循最佳实践,确保代码的健壮性和安全性。
最后,建议在实际项目中对各种拷贝方式进行测试,因为不同的硬件环境、文件系统和 JVM 版本可能会导致性能表现有所不同。只有通过实际测试,才能找到最适合特定环境的最优方案。