一、前言
文件拷贝(传输)涉及到Java中的输入和输出流(InputStream,OutputStream),FileChannel等知识点,把文件拷贝学明白了,IO流的相关知识点在头脑中也会更加清晰。这篇博客介绍几种文件拷贝的方法,其中有一些方法是较为底层的(说白了就是自己手写的方法)在实际生产环境中不推荐使用,但是通过这些较为底层的方法才能更好的理解 “流” 的操作过程,有一些方法是JDK提供的、还有一些方法是各种工具类提供的。JDK提供的和各种工具类提供的,建议在生产环境中使用。
此篇博客主要分为传统的阻塞IO(Blocking I/O)实现的文件拷贝和基于NIO(No-Blocking I/O)的FileChannel方式实现的文件拷贝。
题外话:既然别人已经提供了优秀的文件拷贝方法,我们没有必要去重复造轮子,但是!如果想用好这些工具类,前提是你要明白底层原理。只有这样,当你使用那些优秀的工具类时才能游刃有余地去使用,并且看别人的源码时还可以学习一些编码的新知识,如果只是囫囵吞枣的用别人提供的工具,当时是解决了你的问题,但是总感觉用的一头雾水】
二、BIO拷贝文件的几种方式
1. 拷贝文件 - V1【生产环境不推荐使用】
最原始的文件拷贝方式(使用BIO的方式),也没有使用 try-with-resources 方式管理资源,生产环境不推荐使用。
private static void copyFileV1() throws IOException {
// 源文件
File sourceFile = new File("test.txt");
// 目标文件
File targetFile = new File("test_copy.txt");
FileInputStream fis = new FileInputStream(sourceFile);
FileOutputStream fos = new FileOutputStream(targetFile);
System.out.println("fis.available(): " + fis.available());
byte[] buffer = new byte[1024];
int length;
// int total = 0;
// int number = 0;
while ((length = fis.read(buffer)) != -1) {
// total += length;
// number++;
fos.write(buffer, 0, length);
}
// System.out.println("total bytes: " + total);
// System.out.println("number: " + number);
fos.close();
fis.close();
}
2. 拷贝文件 - V2 【生产环境不推荐使用】
使用BIO的方式,使用try-with-resources的方式释放资源,生产环境不推荐使用。
private static void copyFileV2() throws IOException {
// 源文件
File sourceFile = new File("test.txt");
// 目标文件
File targetFile = new File("test_copy.txt");
try (FileInputStream fis = new FileInputStream(sourceFile);
FileOutputStream fos = new FileOutputStream(targetFile)) {
byte[] buffer = new byte[1024];
int length;
while ((length = fis.read(buffer)) != -1) {
fos.write(buffer, 0, length);
}
}
}
3. 拷贝文件 - V3. 使用JDK提供的Files.copy方法 【生产环境推荐使用】
使用JDK提供的Files.copy工具类。JDK 提供的 Files.copy()
方法本身并没有特定的文件大小限制。这个方法是用于从源通道复制到目标通道,通常用于文件拷贝。拷贝操作的效率和限制更多地取决于底层的文件系统、可用内存、JVM堆大小以及操作系统的限制等因素。
然而,对于大文件的拷贝,使用 Files.copy()
方法可能不是最高效的方式,因为它可能会涉及将整个文件内容加载到内存中。对于大文件,推荐的做法是使用带缓冲区的分块读取和写入,这样可以减少内存消耗并提高性能,参考【3. 拷贝文件 - V2】。
private static void copyFileV3() throws IOException {
Path sourceFile = Paths.get("test.txt");
Path targetFile = Paths.get("test_copy.txt");
Files.copy(sourceFile, targetFile);
//Files.copy(sourceFile, targetFile, StandardCopyOption.COPY_ATTRIBUTES);
}
4. 拷贝文件 - V4. 使用Hutool工具提供的文件拷贝方法 【生产环境推荐使用】
需要导入hutool工具包依赖,如下。其【底层使用的是JDK提供的Files工具类】。
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.8</version> </dependency>
private static void copyFileV4() {
// 源文件
File sourceFile = new File("test.txt");
// 目标文件
File targetFile = new File("test_copy.txt");
FileUtil.copy(sourceFile, targetFile, false);
}
5. 拷贝文件 - V5. 使用google提供的 guava 中的工具类 【生产环境推荐使用】
需要导入 guava工具类。如下。【底层使用的和 copyFileV2 一样的方式,但是使用这个工具类的好处是可以帮我们自动释放流资源, 底层创建的缓冲数组大小是8192个字节】
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>19.0</version> </dependency>
private static void copyFileV5() throws IOException {
// 源文件
File sourceFile = new File("test.txt");
// 目标文件
File targetFile = new File("test_copy.txt");
com.google.common.io.Files.copy(sourceFile, targetFile);
}
三、基于NIO的FileChannel 拷贝文件的几种方式
1. 拷贝文件 - V1. 【生产环境不推荐使用】
使用FileChannel拷贝文件(最基本的使用方式),没有考虑文件大小问题 。
private static void v1() throws IOException {
// 获取文件输入流
File file = new File("test.txt");
FileInputStream fileInputStream = new FileInputStream(file);
// 从文件输入流获取通道
FileChannel inputStreamChannel = fileInputStream.getChannel();
// 获取文件输出流
FileOutputStream fileOutputStream = new FileOutputStream("test_copy.txt");
// 从文件输出流获取通道
FileChannel outputStreamChannel = fileOutputStream.getChannel();
// 创建ByteBuffer, 文件内容不大,这里的演示就一次性读取,部分多次读取了
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
// 把输入流通道的数据读取到数据缓冲区
inputStreamChannel.read(byteBuffer);
// 切换成读模式
byteBuffer.flip();
// 把缓冲区(ByteBuffer)中的数据写入到输出流通道
outputStreamChannel.write(byteBuffer);
// 关闭资源
fileOutputStream.close();
fileInputStream.close();
outputStreamChannel.close();
inputStreamChannel.close();
}
2. 拷贝文件 - V2. 【生产环境不推荐使用】
相比于上面的示例,这个示例代码考虑了文件的大小,使用缓冲区一次读取1024个字节的数据。
private static void v2_1() {
try (FileInputStream fileInputStream = new FileInputStream("test.txt");
FileChannel inputStreamChannel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("test_copy.txt");
FileChannel outputStreamChannel = fileOutputStream.getChannel()) {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while ((inputStreamChannel.read(byteBuffer)) != -1) {
byteBuffer.flip(); // Prepare the buffer for writing
outputStreamChannel.write(byteBuffer);
byteBuffer.clear(); // Prepare the buffer for reading again
}
System.out.println("Large file copied successfully.");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
这个示例和上面的v2_1方法是一样的,只不过获取FileChannel的方式不一样。
private static void v2_2() {
Path source = Paths.get("test.txt");
Path target = Paths.get("test_copy.txt");
try (FileChannel sourceChannel = FileChannel.open(source, StandardOpenOption.READ);
FileChannel targetChannel = FileChannel.open(target, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
ByteBuffer buffer = ByteBuffer.allocate(8 * 1024); // 8 KB buffer
while (sourceChannel.read(buffer) != -1) {
buffer.flip(); // Prepare the buffer for writing
targetChannel.write(buffer);
buffer.clear(); // Prepare the buffer for reading again
}
System.out.println("Large file copied successfully.");
} catch (IOException e) {
e.printStackTrace();
}
}
3. 拷贝文件 - V3. 【生产环境推荐使用】
使用FileChannel的transferTo方法拷贝文件(文件传输,效率比自己写的更高,JDK中方法transferTo的底层都会使用零拷贝进行优化) 【transferTo方法一次性传递的文件大小上限是2G,所以此代码考虑了如果要拷贝的文件大小超过2G的问题。】
private static void v3() {
try (FileInputStream fileInputStream = new FileInputStream("test.txt");
FileChannel inputStreamChannel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("test_copy.txt");
FileChannel outputStreamChannel = fileOutputStream.getChannel()) {
long size = inputStreamChannel.size();
// left变量表示还剩余多少字节要传递
for (long left = size; left > 0; ) {
log.info("position:{},left:{}", (size - left), left);
left = left - inputStreamChannel.transferTo((size - left), left, outputStreamChannel);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}